@limeade-labs/sparkui 1.0.0
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/.env.example +9 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/SKILL.md +242 -0
- package/bin/deploy +23 -0
- package/bin/sparkui.js +390 -0
- package/docs/README.md +51 -0
- package/docs/api-reference.md +428 -0
- package/docs/chatgpt-setup.md +206 -0
- package/docs/components.md +432 -0
- package/docs/getting-started.md +179 -0
- package/docs/mcp-setup.md +195 -0
- package/docs/openclaw-setup.md +177 -0
- package/docs/templates.md +289 -0
- package/lib/components.js +474 -0
- package/lib/store.js +193 -0
- package/lib/templates.js +48 -0
- package/lib/ws-client.js +197 -0
- package/mcp-server/README.md +189 -0
- package/mcp-server/index.js +174 -0
- package/mcp-server/package.json +15 -0
- package/package.json +52 -0
- package/server.js +620 -0
- package/templates/base.js +82 -0
- package/templates/checkout.js +271 -0
- package/templates/feedback-form.js +140 -0
- package/templates/macro-tracker.js +205 -0
- package/templates/workout-timer.js +510 -0
- package/templates/ws-test.js +136 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const base = require('./base');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fake Stripe-style checkout template.
|
|
7
|
+
* 100% cosmetic — no real payment processing. Demo only.
|
|
8
|
+
*
|
|
9
|
+
* Expected data shape:
|
|
10
|
+
* {
|
|
11
|
+
* product: { name, description, price, image?, imageUrl? },
|
|
12
|
+
* shipping: 0,
|
|
13
|
+
* tax: 2.40,
|
|
14
|
+
* currency: "USD" // optional, defaults to USD
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
function checkout(data = {}) {
|
|
18
|
+
const pageId = data._pageId || 'unknown';
|
|
19
|
+
const _og = data._og || {};
|
|
20
|
+
|
|
21
|
+
const product = data.product || {};
|
|
22
|
+
const productName = product.name || 'Product';
|
|
23
|
+
const productDesc = product.description || '';
|
|
24
|
+
const productPrice = typeof product.price === 'number' ? product.price : 29.99;
|
|
25
|
+
const productImage = product.imageUrl || product.image || '📦';
|
|
26
|
+
const isEmoji = !product.imageUrl && productImage.length <= 4;
|
|
27
|
+
|
|
28
|
+
const shipping = typeof data.shipping === 'number' ? data.shipping : 0;
|
|
29
|
+
const tax = typeof data.tax === 'number' ? data.tax : 0;
|
|
30
|
+
const currency = data.currency || 'USD';
|
|
31
|
+
const currencySymbol = currency === 'USD' ? '$' : currency;
|
|
32
|
+
|
|
33
|
+
const subtotal = productPrice;
|
|
34
|
+
const total = (subtotal + shipping + tax).toFixed(2);
|
|
35
|
+
|
|
36
|
+
const body = `
|
|
37
|
+
<!-- DEMO Watermark -->
|
|
38
|
+
<div style="position:fixed;top:0;left:0;right:0;z-index:1000;background:linear-gradient(90deg,#ff6b00,#ff8c00);color:#fff;text-align:center;font-size:0.75rem;padding:4px 0;font-weight:700;letter-spacing:2px">
|
|
39
|
+
⚠️ DEMO MODE — No real charges will be made
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Security Banner -->
|
|
43
|
+
<div style="margin-top:28px;background:#0a2a1a;border:1px solid #00ff8840;border-radius:8px;padding:12px 16px;text-align:center;font-size:0.85rem;color:#00ff88;margin-bottom:24px">
|
|
44
|
+
🔒 Secure checkout — card details never touch your chat
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Product Card -->
|
|
48
|
+
<div style="background:#1a1a1a;border:1px solid #2a2a2a;border-radius:12px;padding:20px;margin-bottom:24px;display:flex;align-items:center;gap:16px">
|
|
49
|
+
<div style="${isEmoji
|
|
50
|
+
? 'font-size:3rem;width:72px;height:72px;display:flex;align-items:center;justify-content:center;background:#111;border-radius:12px;flex-shrink:0'
|
|
51
|
+
: 'width:72px;height:72px;border-radius:12px;overflow:hidden;flex-shrink:0;background:#111'}">
|
|
52
|
+
${isEmoji ? productImage : `<img src="${productImage}" alt="${productName}" style="width:100%;height:100%;object-fit:cover">`}
|
|
53
|
+
</div>
|
|
54
|
+
<div style="flex:1;min-width:0">
|
|
55
|
+
<div style="font-size:1.1rem;font-weight:600;color:#fff;margin-bottom:4px">${productName}</div>
|
|
56
|
+
<div style="font-size:0.85rem;color:#888;line-height:1.4">${productDesc}</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div style="font-size:1.25rem;font-weight:700;color:#fff;flex-shrink:0">${currencySymbol}${productPrice.toFixed(2)}</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Quantity -->
|
|
62
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;padding:0 4px">
|
|
63
|
+
<span style="color:#aaa;font-size:0.9rem">Quantity</span>
|
|
64
|
+
<div style="display:flex;align-items:center;gap:12px">
|
|
65
|
+
<button id="qty-minus" type="button" style="width:32px;height:32px;border-radius:6px;border:1px solid #333;background:#1a1a1a;color:#fff;font-size:1.1rem;cursor:pointer;display:flex;align-items:center;justify-content:center">−</button>
|
|
66
|
+
<span id="qty-display" style="font-size:1rem;font-weight:600;color:#fff;min-width:20px;text-align:center">1</span>
|
|
67
|
+
<button id="qty-plus" type="button" style="width:32px;height:32px;border-radius:6px;border:1px solid #333;background:#1a1a1a;color:#fff;font-size:1.1rem;cursor:pointer;display:flex;align-items:center;justify-content:center">+</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Payment Section -->
|
|
72
|
+
<div style="background:#1a1a1a;border:1px solid #2a2a2a;border-radius:12px;padding:20px;margin-bottom:24px">
|
|
73
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
|
74
|
+
<h2 style="font-size:1rem;font-weight:600;color:#fff;margin:0">Payment details</h2>
|
|
75
|
+
<div style="display:flex;gap:6px;align-items:center">
|
|
76
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#00ff88" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
77
|
+
<span style="font-size:0.75rem;color:#00ff88;font-weight:500">Secure</span>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Card Number -->
|
|
82
|
+
<label style="display:block;font-size:0.8rem;color:#888;margin-bottom:6px">Card number</label>
|
|
83
|
+
<div style="position:relative;margin-bottom:14px">
|
|
84
|
+
<input id="card-number" type="text" value="4242 4242 4242 4242" maxlength="19" style="width:100%;padding:12px 14px;padding-right:48px;border-radius:8px;border:1px solid #333;background:#222;color:#fff;font-size:1rem;font-family:monospace;outline:none;transition:border-color 0.2s" onfocus="this.style.borderColor='#00ff88'" onblur="this.style.borderColor='#333'">
|
|
85
|
+
<div style="position:absolute;right:12px;top:50%;transform:translateY(-50%);display:flex;gap:4px">
|
|
86
|
+
<svg width="24" height="16" viewBox="0 0 24 16" fill="none"><rect width="24" height="16" rx="2" fill="#1a1f71"/><circle cx="9" cy="8" r="5" fill="#eb001b"/><circle cx="15" cy="8" r="5" fill="#f79e1b" opacity="0.8"/></svg>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- Expiry + CVC row -->
|
|
91
|
+
<div style="display:flex;gap:12px;margin-bottom:14px">
|
|
92
|
+
<div style="flex:1">
|
|
93
|
+
<label style="display:block;font-size:0.8rem;color:#888;margin-bottom:6px">Expiry</label>
|
|
94
|
+
<input id="card-expiry" type="text" value="12/28" maxlength="5" style="width:100%;padding:12px 14px;border-radius:8px;border:1px solid #333;background:#222;color:#fff;font-size:1rem;font-family:monospace;outline:none;transition:border-color 0.2s" onfocus="this.style.borderColor='#00ff88'" onblur="this.style.borderColor='#333'">
|
|
95
|
+
</div>
|
|
96
|
+
<div style="flex:1">
|
|
97
|
+
<label style="display:block;font-size:0.8rem;color:#888;margin-bottom:6px">CVC</label>
|
|
98
|
+
<input id="card-cvc" type="text" value="123" maxlength="4" style="width:100%;padding:12px 14px;border-radius:8px;border:1px solid #333;background:#222;color:#fff;font-size:1rem;font-family:monospace;outline:none;transition:border-color 0.2s" onfocus="this.style.borderColor='#00ff88'" onblur="this.style.borderColor='#333'">
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<!-- Cardholder Name -->
|
|
103
|
+
<label style="display:block;font-size:0.8rem;color:#888;margin-bottom:6px">Cardholder name</label>
|
|
104
|
+
<input id="card-name" type="text" placeholder="Full name on card" style="width:100%;padding:12px 14px;border-radius:8px;border:1px solid #333;background:#222;color:#fff;font-size:1rem;outline:none;transition:border-color 0.2s" onfocus="this.style.borderColor='#00ff88'" onblur="this.style.borderColor='#333'">
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Order Summary -->
|
|
108
|
+
<div style="background:#1a1a1a;border:1px solid #2a2a2a;border-radius:12px;padding:20px;margin-bottom:24px">
|
|
109
|
+
<h2 style="font-size:1rem;font-weight:600;color:#fff;margin-bottom:16px">Order summary</h2>
|
|
110
|
+
<div style="display:flex;justify-content:space-between;margin-bottom:8px;font-size:0.9rem">
|
|
111
|
+
<span style="color:#aaa">Subtotal</span>
|
|
112
|
+
<span id="summary-subtotal" style="color:#ccc">${currencySymbol}${subtotal.toFixed(2)}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div style="display:flex;justify-content:space-between;margin-bottom:8px;font-size:0.9rem">
|
|
115
|
+
<span style="color:#aaa">Shipping</span>
|
|
116
|
+
<span style="color:#ccc">${shipping === 0 ? 'Free' : currencySymbol + shipping.toFixed(2)}</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div style="display:flex;justify-content:space-between;margin-bottom:12px;font-size:0.9rem">
|
|
119
|
+
<span style="color:#aaa">Tax</span>
|
|
120
|
+
<span style="color:#ccc">${currencySymbol}${tax.toFixed(2)}</span>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Promo Code -->
|
|
124
|
+
<div style="display:flex;gap:8px;margin-bottom:16px">
|
|
125
|
+
<input type="text" placeholder="Promo code" style="flex:1;padding:8px 12px;border-radius:6px;border:1px solid #333;background:#222;color:#fff;font-size:0.85rem;outline:none" onfocus="this.style.borderColor='#00ff88'" onblur="this.style.borderColor='#333'">
|
|
126
|
+
<button type="button" style="padding:8px 16px;border-radius:6px;border:1px solid #333;background:#222;color:#888;font-size:0.85rem;cursor:pointer" onclick="this.textContent='Coming soon';this.style.color='#00ff88';setTimeout(()=>{this.textContent='Apply';this.style.color='#888'},2000)">Apply</button>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div style="border-top:1px solid #333;padding-top:12px;display:flex;justify-content:space-between;align-items:center">
|
|
130
|
+
<span style="font-size:1rem;font-weight:600;color:#fff">Total</span>
|
|
131
|
+
<span id="summary-total" style="font-size:1.25rem;font-weight:700;color:#fff">${currencySymbol}${total}</span>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<!-- Pay Button -->
|
|
136
|
+
<button id="pay-btn" type="button" style="width:100%;padding:16px;border-radius:10px;border:none;background:linear-gradient(135deg,#00cc6a,#00ff88);color:#000;font-size:1.1rem;font-weight:700;cursor:pointer;transition:all 0.2s;position:relative;overflow:hidden" onmouseover="this.style.transform='translateY(-1px)';this.style.boxShadow='0 4px 20px #00ff8840'" onmouseout="this.style.transform='';this.style.boxShadow=''">
|
|
137
|
+
<span id="pay-text">Pay ${currencySymbol}${total}</span>
|
|
138
|
+
<div id="pay-spinner" style="display:none;position:absolute;inset:0;background:inherit;display:none;align-items:center;justify-content:center">
|
|
139
|
+
<div style="width:24px;height:24px;border:3px solid rgba(0,0,0,0.2);border-top-color:#000;border-radius:50%;animation:spin 0.8s linear infinite"></div>
|
|
140
|
+
</div>
|
|
141
|
+
</button>
|
|
142
|
+
|
|
143
|
+
<!-- Success State (hidden) -->
|
|
144
|
+
<div id="success-overlay" style="display:none;position:fixed;inset:0;background:#111;z-index:2000;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:24px">
|
|
145
|
+
<div id="checkmark-container" style="width:80px;height:80px;margin-bottom:24px">
|
|
146
|
+
<svg viewBox="0 0 80 80" style="width:80px;height:80px">
|
|
147
|
+
<circle cx="40" cy="40" r="36" fill="none" stroke="#00ff88" stroke-width="3" stroke-dasharray="226" stroke-dashoffset="226" style="animation:circle-draw 0.6s ease forwards"/>
|
|
148
|
+
<path d="M24 42 L35 53 L56 28" fill="none" stroke="#00ff88" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="60" stroke-dashoffset="60" style="animation:check-draw 0.4s ease 0.5s forwards"/>
|
|
149
|
+
</svg>
|
|
150
|
+
</div>
|
|
151
|
+
<h2 style="font-size:1.5rem;font-weight:700;color:#fff;margin-bottom:8px">Payment successful!</h2>
|
|
152
|
+
<p id="success-order-id" style="color:#00ff88;font-size:0.9rem;margin-bottom:8px"></p>
|
|
153
|
+
<p style="color:#888;font-size:0.9rem;margin-bottom:32px">Your agent has been notified. You can close this page.</p>
|
|
154
|
+
<div style="background:#1a1a1a;border:1px solid #2a2a2a;border-radius:10px;padding:16px 24px;display:inline-flex;align-items:center;gap:12px">
|
|
155
|
+
<span style="font-size:1.5rem">${isEmoji ? productImage : '✅'}</span>
|
|
156
|
+
<div style="text-align:left">
|
|
157
|
+
<div style="font-size:0.95rem;font-weight:600;color:#fff">${productName}</div>
|
|
158
|
+
<div id="success-total" style="font-size:0.85rem;color:#00ff88"></div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- Footer -->
|
|
164
|
+
<div style="text-align:center;margin-top:20px;padding:16px 0">
|
|
165
|
+
<span style="font-size:0.8rem;color:#555">Powered by SparkUI ⚡</span>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<style>
|
|
169
|
+
@keyframes spin {
|
|
170
|
+
to { transform: rotate(360deg); }
|
|
171
|
+
}
|
|
172
|
+
@keyframes circle-draw {
|
|
173
|
+
to { stroke-dashoffset: 0; }
|
|
174
|
+
}
|
|
175
|
+
@keyframes check-draw {
|
|
176
|
+
to { stroke-dashoffset: 0; }
|
|
177
|
+
}
|
|
178
|
+
input::placeholder { color: #555; }
|
|
179
|
+
</style>
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
const extraHead = `
|
|
183
|
+
<script>
|
|
184
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
185
|
+
var quantity = 1;
|
|
186
|
+
var unitPrice = ${productPrice};
|
|
187
|
+
var shipping = ${shipping};
|
|
188
|
+
var tax = ${tax};
|
|
189
|
+
var symbol = '${currencySymbol}';
|
|
190
|
+
var productName = ${JSON.stringify(productName)};
|
|
191
|
+
|
|
192
|
+
var qtyDisplay = document.getElementById('qty-display');
|
|
193
|
+
var summarySubtotal = document.getElementById('summary-subtotal');
|
|
194
|
+
var summaryTotal = document.getElementById('summary-total');
|
|
195
|
+
var payText = document.getElementById('pay-text');
|
|
196
|
+
|
|
197
|
+
function updateTotals() {
|
|
198
|
+
var sub = (unitPrice * quantity);
|
|
199
|
+
var tot = (sub + shipping + tax).toFixed(2);
|
|
200
|
+
summarySubtotal.textContent = symbol + sub.toFixed(2);
|
|
201
|
+
summaryTotal.textContent = symbol + tot;
|
|
202
|
+
payText.textContent = 'Pay ' + symbol + tot;
|
|
203
|
+
qtyDisplay.textContent = quantity;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
document.getElementById('qty-minus').addEventListener('click', function() {
|
|
207
|
+
if (quantity > 1) { quantity--; updateTotals(); }
|
|
208
|
+
});
|
|
209
|
+
document.getElementById('qty-plus').addEventListener('click', function() {
|
|
210
|
+
if (quantity < 99) { quantity++; updateTotals(); }
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Pay button
|
|
214
|
+
document.getElementById('pay-btn').addEventListener('click', function() {
|
|
215
|
+
var btn = this;
|
|
216
|
+
var spinner = document.getElementById('pay-spinner');
|
|
217
|
+
var text = document.getElementById('pay-text');
|
|
218
|
+
|
|
219
|
+
// Disable + show spinner
|
|
220
|
+
btn.disabled = true;
|
|
221
|
+
btn.style.cursor = 'not-allowed';
|
|
222
|
+
text.style.visibility = 'hidden';
|
|
223
|
+
spinner.style.display = 'flex';
|
|
224
|
+
|
|
225
|
+
// Simulate processing
|
|
226
|
+
setTimeout(function() {
|
|
227
|
+
var orderId = 'ORD-' + Math.random().toString(36).slice(2, 8).toUpperCase();
|
|
228
|
+
var sub = (unitPrice * quantity);
|
|
229
|
+
var tot = (sub + shipping + tax).toFixed(2);
|
|
230
|
+
|
|
231
|
+
// Send completion event via WS
|
|
232
|
+
if (window.sparkui && sparkui.sendCompletion) {
|
|
233
|
+
sparkui.sendCompletion({
|
|
234
|
+
action: 'checkout_complete',
|
|
235
|
+
orderId: orderId,
|
|
236
|
+
product: productName,
|
|
237
|
+
quantity: quantity,
|
|
238
|
+
total: parseFloat(tot),
|
|
239
|
+
status: 'success',
|
|
240
|
+
completedAt: new Date().toISOString()
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Show success
|
|
245
|
+
document.getElementById('success-order-id').textContent = 'Order ' + orderId;
|
|
246
|
+
document.getElementById('success-total').textContent = symbol + tot;
|
|
247
|
+
var overlay = document.getElementById('success-overlay');
|
|
248
|
+
overlay.style.display = 'flex';
|
|
249
|
+
}, 2000);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
</script>
|
|
253
|
+
`;
|
|
254
|
+
|
|
255
|
+
const og = {
|
|
256
|
+
title: `Secure Checkout — ${productName}`,
|
|
257
|
+
description: 'Complete your purchase securely',
|
|
258
|
+
image: _og.image,
|
|
259
|
+
url: _og.url,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
return base({
|
|
263
|
+
title: `Checkout — ${productName}`,
|
|
264
|
+
body,
|
|
265
|
+
id: pageId,
|
|
266
|
+
extraHead,
|
|
267
|
+
og,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = checkout;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const base = require('./base');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Feedback form template.
|
|
7
|
+
* Simple form: rating 1-5, text feedback, submit.
|
|
8
|
+
* Sends a completion event with form data on submit.
|
|
9
|
+
*/
|
|
10
|
+
function feedbackForm(data = {}) {
|
|
11
|
+
const pageId = data._pageId || 'unknown';
|
|
12
|
+
const title = data.title || 'Feedback';
|
|
13
|
+
const subtitle = data.subtitle || 'We\'d love to hear from you.';
|
|
14
|
+
const questions = data.questions || null; // optional array of extra text fields
|
|
15
|
+
const _og = data._og || {};
|
|
16
|
+
|
|
17
|
+
let extraFields = '';
|
|
18
|
+
let extraFieldsJs = '';
|
|
19
|
+
if (questions && Array.isArray(questions)) {
|
|
20
|
+
questions.forEach((q, i) => {
|
|
21
|
+
const fieldId = `extra-field-${i}`;
|
|
22
|
+
extraFields += `
|
|
23
|
+
<label style="display:block;margin-bottom:6px;color:#aaa;font-size:0.9rem">${q}</label>
|
|
24
|
+
<input id="${fieldId}" type="text" placeholder="Your answer" style="width:100%;padding:10px 12px;border-radius:6px;border:1px solid #333;background:#1a1a1a;color:#eee;font-size:1rem;margin-bottom:16px;outline:none" onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#333'">
|
|
25
|
+
`;
|
|
26
|
+
extraFieldsJs += `formData['${q.replace(/'/g, "\\'")}'] = document.getElementById('${fieldId}').value;\n`;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const body = `
|
|
31
|
+
<div style="text-align:center;margin-bottom:24px">
|
|
32
|
+
<h1 style="font-size:1.5rem;margin-bottom:6px">📝 ${title}</h1>
|
|
33
|
+
<p style="color:#888;font-size:0.95rem">${subtitle}</p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<form id="feedback-form" style="background:#1a1a1a;padding:20px;border-radius:12px;border:1px solid #222">
|
|
37
|
+
<!-- Rating -->
|
|
38
|
+
<label style="display:block;margin-bottom:10px;color:#aaa;font-size:0.9rem">Rating</label>
|
|
39
|
+
<div id="star-rating" style="display:flex;gap:8px;margin-bottom:20px;justify-content:center">
|
|
40
|
+
<button type="button" class="star-btn" data-value="1" style="font-size:2rem;background:none;border:none;cursor:pointer;opacity:0.3;transition:opacity 0.2s">⭐</button>
|
|
41
|
+
<button type="button" class="star-btn" data-value="2" style="font-size:2rem;background:none;border:none;cursor:pointer;opacity:0.3;transition:opacity 0.2s">⭐</button>
|
|
42
|
+
<button type="button" class="star-btn" data-value="3" style="font-size:2rem;background:none;border:none;cursor:pointer;opacity:0.3;transition:opacity 0.2s">⭐</button>
|
|
43
|
+
<button type="button" class="star-btn" data-value="4" style="font-size:2rem;background:none;border:none;cursor:pointer;opacity:0.3;transition:opacity 0.2s">⭐</button>
|
|
44
|
+
<button type="button" class="star-btn" data-value="5" style="font-size:2rem;background:none;border:none;cursor:pointer;opacity:0.3;transition:opacity 0.2s">⭐</button>
|
|
45
|
+
</div>
|
|
46
|
+
<input type="hidden" id="rating-value" value="0">
|
|
47
|
+
|
|
48
|
+
<!-- Feedback text -->
|
|
49
|
+
<label style="display:block;margin-bottom:6px;color:#aaa;font-size:0.9rem">Feedback</label>
|
|
50
|
+
<textarea id="feedback-text" rows="4" placeholder="Tell us what you think..." style="width:100%;padding:10px 12px;border-radius:6px;border:1px solid #333;background:#111;color:#eee;font-size:1rem;margin-bottom:16px;resize:vertical;outline:none;font-family:inherit" onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#333'"></textarea>
|
|
51
|
+
|
|
52
|
+
${extraFields}
|
|
53
|
+
|
|
54
|
+
<!-- Submit -->
|
|
55
|
+
<button type="submit" id="submit-btn" style="width:100%;padding:12px;border-radius:8px;border:none;background:#059669;color:#fff;font-size:1.05rem;cursor:pointer;font-weight:600;transition:background 0.2s" onmouseover="this.style.background='#047857'" onmouseout="this.style.background='#059669'">
|
|
56
|
+
Submit Feedback
|
|
57
|
+
</button>
|
|
58
|
+
</form>
|
|
59
|
+
|
|
60
|
+
<!-- Success state (hidden) -->
|
|
61
|
+
<div id="success-msg" style="display:none;text-align:center;padding:40px 20px">
|
|
62
|
+
<div style="font-size:3rem;margin-bottom:12px">✅</div>
|
|
63
|
+
<h2 style="font-size:1.3rem;margin-bottom:8px">Thank you!</h2>
|
|
64
|
+
<p style="color:#888">Your feedback has been submitted.</p>
|
|
65
|
+
</div>
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
const extraHead = `
|
|
69
|
+
<script>
|
|
70
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
71
|
+
var selectedRating = 0;
|
|
72
|
+
var stars = document.querySelectorAll('.star-btn');
|
|
73
|
+
|
|
74
|
+
function updateStars(value) {
|
|
75
|
+
stars.forEach(function(s) {
|
|
76
|
+
s.style.opacity = parseInt(s.getAttribute('data-value')) <= value ? '1' : '0.3';
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
stars.forEach(function(star) {
|
|
81
|
+
star.addEventListener('click', function() {
|
|
82
|
+
selectedRating = parseInt(this.getAttribute('data-value'));
|
|
83
|
+
document.getElementById('rating-value').value = selectedRating;
|
|
84
|
+
updateStars(selectedRating);
|
|
85
|
+
});
|
|
86
|
+
star.addEventListener('mouseenter', function() {
|
|
87
|
+
updateStars(parseInt(this.getAttribute('data-value')));
|
|
88
|
+
});
|
|
89
|
+
star.addEventListener('mouseleave', function() {
|
|
90
|
+
updateStars(selectedRating);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
document.getElementById('feedback-form').addEventListener('submit', function(e) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
|
|
97
|
+
var rating = parseInt(document.getElementById('rating-value').value);
|
|
98
|
+
if (rating === 0) {
|
|
99
|
+
alert('Please select a rating');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
var formData = {
|
|
104
|
+
rating: rating,
|
|
105
|
+
feedback: document.getElementById('feedback-text').value,
|
|
106
|
+
submittedAt: new Date().toISOString()
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
${extraFieldsJs}
|
|
110
|
+
|
|
111
|
+
// Send completion via WS
|
|
112
|
+
if (window.sparkui) {
|
|
113
|
+
sparkui.sendCompletion(formData);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Show success
|
|
117
|
+
document.getElementById('feedback-form').style.display = 'none';
|
|
118
|
+
document.getElementById('success-msg').style.display = 'block';
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
</script>
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
const og = {
|
|
125
|
+
title: _og.title || title,
|
|
126
|
+
description: _og.description || 'Quick feedback form — share your thoughts ⚡',
|
|
127
|
+
image: _og.image,
|
|
128
|
+
url: _og.url,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return base({
|
|
132
|
+
title,
|
|
133
|
+
body,
|
|
134
|
+
id: pageId,
|
|
135
|
+
extraHead,
|
|
136
|
+
og,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = feedbackForm;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const base = require('./base');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Macro Tracker template.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} data
|
|
9
|
+
* @param {string} data.date - e.g. "2026-03-10"
|
|
10
|
+
* @param {object} data.calories - { current, target }
|
|
11
|
+
* @param {object} data.protein - { current, target }
|
|
12
|
+
* @param {object} data.fat - { current, target }
|
|
13
|
+
* @param {object} data.carbs - { current, target }
|
|
14
|
+
* @param {Array} [data.meals] - [{ name, calories, time }]
|
|
15
|
+
* @param {string} [data._pageId] - injected by template engine
|
|
16
|
+
* @returns {string} Full HTML page
|
|
17
|
+
*/
|
|
18
|
+
function macroTracker(data) {
|
|
19
|
+
const { date, calories, protein, fat, carbs, meals = [], _pageId = '', _og = {} } = data;
|
|
20
|
+
|
|
21
|
+
const macros = [
|
|
22
|
+
{ label: 'Calories', ...calories, unit: 'cal', color: '#00d4aa', icon: '🔥' },
|
|
23
|
+
{ label: 'Protein', ...protein, unit: 'g', color: '#6c63ff', icon: '💪' },
|
|
24
|
+
{ label: 'Fat', ...fat, unit: 'g', color: '#ff6b6b', icon: '🥑' },
|
|
25
|
+
{ label: 'Carbs', ...carbs, unit: 'g', color: '#ffd93d', icon: '⚡' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function pct(current, target) {
|
|
29
|
+
if (!target) return 0;
|
|
30
|
+
return Math.min(100, Math.round((current / target) * 100));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatDate(dateStr) {
|
|
34
|
+
try {
|
|
35
|
+
const d = new Date(dateStr + 'T12:00:00');
|
|
36
|
+
return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
|
|
37
|
+
} catch {
|
|
38
|
+
return dateStr;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const macroCards = macros.map(m => {
|
|
43
|
+
const p = pct(m.current, m.target);
|
|
44
|
+
const isOver = m.current > m.target;
|
|
45
|
+
return `
|
|
46
|
+
<div class="macro-card">
|
|
47
|
+
<div class="macro-header">
|
|
48
|
+
<span class="macro-icon">${m.icon}</span>
|
|
49
|
+
<span class="macro-label">${m.label}</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="macro-value">
|
|
52
|
+
<span class="macro-current" style="color:${m.color}">${m.current ?? 0}</span>
|
|
53
|
+
<span class="macro-unit">${m.unit}</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="macro-target">of ${m.target} ${m.unit}</div>
|
|
56
|
+
<div class="progress-track">
|
|
57
|
+
<div class="progress-fill ${isOver ? 'over' : ''}" style="width:${p}%;background:${m.color}"></div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="macro-pct">${p}%</div>
|
|
60
|
+
</div>`;
|
|
61
|
+
}).join('\n');
|
|
62
|
+
|
|
63
|
+
const mealRows = meals.map(meal => `
|
|
64
|
+
<div class="meal-row">
|
|
65
|
+
<div class="meal-info">
|
|
66
|
+
<span class="meal-name">${meal.name}</span>
|
|
67
|
+
<span class="meal-time">${meal.time || ''}</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="meal-cal">${meal.calories} cal</div>
|
|
70
|
+
</div>`).join('\n');
|
|
71
|
+
|
|
72
|
+
const mealSection = meals.length > 0 ? `
|
|
73
|
+
<div class="section-header">
|
|
74
|
+
<span class="section-icon">🍽️</span>
|
|
75
|
+
<span>Meals</span>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="meals-list">
|
|
78
|
+
${mealRows}
|
|
79
|
+
</div>` : '';
|
|
80
|
+
|
|
81
|
+
const totalCal = meals.reduce((sum, m) => sum + (m.calories || 0), 0);
|
|
82
|
+
const remaining = (calories.target || 0) - (calories.current || 0);
|
|
83
|
+
|
|
84
|
+
const summaryBar = `
|
|
85
|
+
<div class="summary-bar">
|
|
86
|
+
<div class="summary-item">
|
|
87
|
+
<div class="summary-value">${totalCal}</div>
|
|
88
|
+
<div class="summary-label">Eaten</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="summary-divider"></div>
|
|
91
|
+
<div class="summary-item">
|
|
92
|
+
<div class="summary-value" style="color:${remaining >= 0 ? '#00d4aa' : '#ff6b6b'}">${remaining >= 0 ? remaining : Math.abs(remaining)}</div>
|
|
93
|
+
<div class="summary-label">${remaining >= 0 ? 'Remaining' : 'Over'}</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="summary-divider"></div>
|
|
96
|
+
<div class="summary-item">
|
|
97
|
+
<div class="summary-value">${calories.target || 0}</div>
|
|
98
|
+
<div class="summary-label">Goal</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>`;
|
|
101
|
+
|
|
102
|
+
const body = `
|
|
103
|
+
<div class="tracker-header">
|
|
104
|
+
<h1>📊 Macro Tracker</h1>
|
|
105
|
+
<div class="tracker-date">${formatDate(date)}</div>
|
|
106
|
+
</div>
|
|
107
|
+
${summaryBar}
|
|
108
|
+
<div class="macro-grid">
|
|
109
|
+
${macroCards}
|
|
110
|
+
</div>
|
|
111
|
+
${mealSection}
|
|
112
|
+
<div class="tracker-footer">
|
|
113
|
+
<span class="sparkui-badge">⚡ SparkUI</span>
|
|
114
|
+
<span class="update-time">Updated ${new Date().toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}</span>
|
|
115
|
+
</div>
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
const extraHead = `<style>
|
|
119
|
+
.tracker-header { text-align: center; margin-bottom: 24px; }
|
|
120
|
+
.tracker-header h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: 4px; }
|
|
121
|
+
.tracker-date { color: #888; font-size: 0.9rem; }
|
|
122
|
+
|
|
123
|
+
.summary-bar {
|
|
124
|
+
display: flex; align-items: center; justify-content: center;
|
|
125
|
+
background: #1a1a1a; border-radius: 16px; padding: 16px 20px;
|
|
126
|
+
margin-bottom: 24px; gap: 20px;
|
|
127
|
+
}
|
|
128
|
+
.summary-item { text-align: center; flex: 1; }
|
|
129
|
+
.summary-value { font-size: 1.4rem; font-weight: 700; color: #fff; }
|
|
130
|
+
.summary-label { font-size: 0.75rem; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }
|
|
131
|
+
.summary-divider { width: 1px; height: 36px; background: #333; }
|
|
132
|
+
|
|
133
|
+
.macro-grid {
|
|
134
|
+
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
|
|
135
|
+
margin-bottom: 28px;
|
|
136
|
+
}
|
|
137
|
+
.macro-card {
|
|
138
|
+
background: #1a1a1a; border-radius: 16px; padding: 16px;
|
|
139
|
+
transition: transform 0.15s ease;
|
|
140
|
+
}
|
|
141
|
+
.macro-header { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
|
|
142
|
+
.macro-icon { font-size: 1.1rem; }
|
|
143
|
+
.macro-label { font-size: 0.8rem; color: #aaa; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
144
|
+
.macro-value { display: flex; align-items: baseline; gap: 4px; }
|
|
145
|
+
.macro-current { font-size: 2rem; font-weight: 800; line-height: 1.1; }
|
|
146
|
+
.macro-unit { font-size: 0.8rem; color: #666; }
|
|
147
|
+
.macro-target { font-size: 0.8rem; color: #666; margin-bottom: 10px; }
|
|
148
|
+
.progress-track {
|
|
149
|
+
width: 100%; height: 6px; background: #2a2a2a; border-radius: 3px; overflow: hidden;
|
|
150
|
+
}
|
|
151
|
+
.progress-fill {
|
|
152
|
+
height: 100%; border-radius: 3px; transition: width 0.5s ease;
|
|
153
|
+
}
|
|
154
|
+
.progress-fill.over { opacity: 0.8; animation: pulse 1.5s ease-in-out infinite; }
|
|
155
|
+
@keyframes pulse { 0%,100% { opacity: 0.8; } 50% { opacity: 1; } }
|
|
156
|
+
.macro-pct { font-size: 0.75rem; color: #666; text-align: right; margin-top: 4px; }
|
|
157
|
+
|
|
158
|
+
.section-header {
|
|
159
|
+
display: flex; align-items: center; gap: 8px;
|
|
160
|
+
font-size: 1rem; font-weight: 600; margin-bottom: 12px; color: #ccc;
|
|
161
|
+
}
|
|
162
|
+
.section-icon { font-size: 1.1rem; }
|
|
163
|
+
.meals-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 24px; }
|
|
164
|
+
.meal-row {
|
|
165
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
166
|
+
background: #1a1a1a; border-radius: 12px; padding: 12px 16px;
|
|
167
|
+
}
|
|
168
|
+
.meal-info { display: flex; flex-direction: column; }
|
|
169
|
+
.meal-name { font-weight: 500; font-size: 0.95rem; }
|
|
170
|
+
.meal-time { font-size: 0.8rem; color: #666; }
|
|
171
|
+
.meal-cal { font-weight: 600; color: #00d4aa; font-size: 0.95rem; white-space: nowrap; }
|
|
172
|
+
|
|
173
|
+
.tracker-footer {
|
|
174
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
175
|
+
padding-top: 16px; border-top: 1px solid #222;
|
|
176
|
+
}
|
|
177
|
+
.sparkui-badge {
|
|
178
|
+
font-size: 0.75rem; color: #555; font-weight: 500;
|
|
179
|
+
}
|
|
180
|
+
.update-time { font-size: 0.75rem; color: #555; }
|
|
181
|
+
</style>`;
|
|
182
|
+
|
|
183
|
+
// Template provides richer defaults than the generic ones from server.js
|
|
184
|
+
const defaultDesc = `Daily macro tracking for ${formatDate(date)} — ${calories.current || 0}/${calories.target || 0} cal`;
|
|
185
|
+
const isGenericDesc = !_og.description || _og.description === 'An ephemeral micro-app powered by SparkUI ⚡';
|
|
186
|
+
const isGenericTitle = !_og.title || _og.title === 'Macro Tracker';
|
|
187
|
+
|
|
188
|
+
const og = {
|
|
189
|
+
title: isGenericTitle ? `Macro Tracker — ${formatDate(date)}` : _og.title,
|
|
190
|
+
description: isGenericDesc ? defaultDesc : _og.description,
|
|
191
|
+
image: _og.image,
|
|
192
|
+
url: _og.url,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return base({
|
|
196
|
+
title: `Macros — ${date}`,
|
|
197
|
+
body,
|
|
198
|
+
id: _pageId,
|
|
199
|
+
refreshSeconds: 30,
|
|
200
|
+
extraHead,
|
|
201
|
+
og,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = macroTracker;
|