@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,510 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const base = require('./base');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Workout Timer template.
|
|
7
|
+
* A polished, fitness-app-quality workout page with rounds, rest timer,
|
|
8
|
+
* checklists for warmup/cooldown, and completion tracking.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} data
|
|
11
|
+
* @param {string} data.title - Workout title
|
|
12
|
+
* @param {string} [data.subtitle] - Subtitle (e.g. day/program)
|
|
13
|
+
* @param {Array} [data.warmup] - [{text}] warmup items
|
|
14
|
+
* @param {Array} data.exercises - [{name, reps, notes}]
|
|
15
|
+
* @param {number} [data.rounds=3] - Number of rounds
|
|
16
|
+
* @param {number} [data.restSeconds=60] - Rest duration between rounds
|
|
17
|
+
* @param {Array} [data.cooldown] - [{text}] cooldown items
|
|
18
|
+
* @param {number} [data.estimatedMinutes] - Estimated duration
|
|
19
|
+
* @param {number} [data.estimatedCalories] - Estimated calories
|
|
20
|
+
* @param {string} [data._pageId] - Injected by template engine
|
|
21
|
+
* @param {object} [data._og] - OG metadata
|
|
22
|
+
* @returns {string} Full HTML page
|
|
23
|
+
*/
|
|
24
|
+
function workoutTimer(data) {
|
|
25
|
+
const {
|
|
26
|
+
title = 'Workout',
|
|
27
|
+
subtitle = '',
|
|
28
|
+
warmup = [],
|
|
29
|
+
exercises = [],
|
|
30
|
+
rounds = 3,
|
|
31
|
+
restSeconds = 60,
|
|
32
|
+
cooldown = [],
|
|
33
|
+
estimatedMinutes = 0,
|
|
34
|
+
estimatedCalories = 0,
|
|
35
|
+
_pageId = '',
|
|
36
|
+
_og = {},
|
|
37
|
+
} = data;
|
|
38
|
+
|
|
39
|
+
const totalExercises = exercises.length * rounds + warmup.length + cooldown.length;
|
|
40
|
+
const accentColor = '#00ff88';
|
|
41
|
+
const accentDim = '#00cc6a';
|
|
42
|
+
|
|
43
|
+
// ── Build the HTML body ──
|
|
44
|
+
|
|
45
|
+
const body = `
|
|
46
|
+
<style>
|
|
47
|
+
.wt-header { margin-bottom: 20px; }
|
|
48
|
+
.wt-header h1 {
|
|
49
|
+
font-size: 1.6rem; font-weight: 800; color: #e0e0e0; margin: 0; line-height: 1.3;
|
|
50
|
+
}
|
|
51
|
+
.wt-header .wt-sub {
|
|
52
|
+
color: #888; font-size: 0.9rem; margin-top: 4px;
|
|
53
|
+
}
|
|
54
|
+
.wt-stats {
|
|
55
|
+
display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;
|
|
56
|
+
margin-bottom: 24px;
|
|
57
|
+
}
|
|
58
|
+
.wt-stat {
|
|
59
|
+
background: #1a1a1a; border: 1px solid #333; border-radius: 8px;
|
|
60
|
+
padding: 12px 8px; text-align: center;
|
|
61
|
+
}
|
|
62
|
+
.wt-stat-icon { font-size: 1.2rem; margin-bottom: 2px; }
|
|
63
|
+
.wt-stat-val { font-size: 1.3rem; font-weight: 700; color: #e0e0e0; font-variant-numeric: tabular-nums; }
|
|
64
|
+
.wt-stat-label { font-size: 0.7rem; color: #888; margin-top: 2px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
65
|
+
|
|
66
|
+
.wt-section { margin-bottom: 24px; }
|
|
67
|
+
.wt-section-title {
|
|
68
|
+
font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;
|
|
69
|
+
color: #888; margin-bottom: 10px; display: flex; align-items: center; gap: 8px;
|
|
70
|
+
}
|
|
71
|
+
.wt-section-title::after {
|
|
72
|
+
content: ''; flex: 1; height: 1px; background: #333;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Checklist items */
|
|
76
|
+
.wt-check-item {
|
|
77
|
+
display: flex; align-items: center; padding: 12px 14px; margin-bottom: 6px;
|
|
78
|
+
background: #1a1a1a; border: 1px solid #333; border-radius: 8px;
|
|
79
|
+
cursor: pointer; transition: all 0.2s; user-select: none;
|
|
80
|
+
-webkit-tap-highlight-color: transparent;
|
|
81
|
+
}
|
|
82
|
+
.wt-check-item:active { transform: scale(0.98); }
|
|
83
|
+
.wt-check-item.checked { opacity: 0.5; }
|
|
84
|
+
.wt-check-item.checked .wt-check-text { text-decoration: line-through; }
|
|
85
|
+
.wt-check-circle {
|
|
86
|
+
width: 22px; height: 22px; border-radius: 50%; border: 2px solid #444;
|
|
87
|
+
display: flex; align-items: center; justify-content: center;
|
|
88
|
+
margin-right: 12px; flex-shrink: 0; transition: all 0.2s;
|
|
89
|
+
font-size: 13px; color: transparent;
|
|
90
|
+
}
|
|
91
|
+
.wt-check-item.checked .wt-check-circle {
|
|
92
|
+
border-color: ${accentColor}; color: ${accentColor};
|
|
93
|
+
}
|
|
94
|
+
.wt-check-text { color: #e0e0e0; font-size: 0.95rem; transition: all 0.2s; }
|
|
95
|
+
|
|
96
|
+
/* Round counter */
|
|
97
|
+
.wt-round-bar {
|
|
98
|
+
display: flex; align-items: center; justify-content: center; gap: 10px;
|
|
99
|
+
margin-bottom: 16px; padding: 14px; background: #1a1a1a;
|
|
100
|
+
border: 1px solid #333; border-radius: 8px;
|
|
101
|
+
}
|
|
102
|
+
.wt-round-dot {
|
|
103
|
+
width: 32px; height: 32px; border-radius: 50%; border: 2px solid #444;
|
|
104
|
+
display: flex; align-items: center; justify-content: center;
|
|
105
|
+
font-size: 0.85rem; font-weight: 700; color: #888;
|
|
106
|
+
transition: all 0.3s;
|
|
107
|
+
}
|
|
108
|
+
.wt-round-dot.active {
|
|
109
|
+
border-color: ${accentColor}; color: #111; background: ${accentColor};
|
|
110
|
+
box-shadow: 0 0 12px ${accentColor}40;
|
|
111
|
+
}
|
|
112
|
+
.wt-round-dot.done {
|
|
113
|
+
border-color: ${accentDim}; color: ${accentDim}; background: transparent;
|
|
114
|
+
}
|
|
115
|
+
.wt-round-label {
|
|
116
|
+
font-size: 0.85rem; color: #888; margin-left: 8px; font-weight: 600;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Exercise cards */
|
|
120
|
+
.wt-exercise {
|
|
121
|
+
background: #1a1a1a; border: 1px solid #333; border-radius: 8px;
|
|
122
|
+
border-left: 3px solid ${accentColor}; padding: 16px 16px 16px 18px;
|
|
123
|
+
margin-bottom: 8px; transition: all 0.3s;
|
|
124
|
+
}
|
|
125
|
+
.wt-exercise-name {
|
|
126
|
+
font-size: 1.05rem; font-weight: 700; color: #e0e0e0; margin-bottom: 4px;
|
|
127
|
+
}
|
|
128
|
+
.wt-exercise-reps {
|
|
129
|
+
font-size: 0.95rem; color: ${accentColor}; font-weight: 600; margin-bottom: 4px;
|
|
130
|
+
}
|
|
131
|
+
.wt-exercise-notes {
|
|
132
|
+
font-size: 0.85rem; color: #888; font-style: italic;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Rest timer */
|
|
136
|
+
.wt-rest-panel {
|
|
137
|
+
background: #1a1a1a; border: 1px solid #333; border-radius: 12px;
|
|
138
|
+
padding: 24px; text-align: center; margin-bottom: 16px;
|
|
139
|
+
display: none;
|
|
140
|
+
}
|
|
141
|
+
.wt-rest-panel.visible { display: block; animation: wtFadeIn 0.3s ease; }
|
|
142
|
+
.wt-rest-display {
|
|
143
|
+
font-size: 3.5rem; font-weight: 800; font-variant-numeric: tabular-nums;
|
|
144
|
+
color: #e0e0e0; margin: 12px 0;
|
|
145
|
+
}
|
|
146
|
+
.wt-rest-label { font-size: 0.85rem; color: #888; text-transform: uppercase; letter-spacing: 1px; }
|
|
147
|
+
.wt-rest-progress {
|
|
148
|
+
height: 4px; background: #333; border-radius: 2px; margin: 16px 0;
|
|
149
|
+
overflow: hidden;
|
|
150
|
+
}
|
|
151
|
+
.wt-rest-bar {
|
|
152
|
+
height: 100%; background: ${accentColor}; width: 0%; transition: width 1s linear;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Buttons */
|
|
156
|
+
.wt-btn {
|
|
157
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
158
|
+
padding: 14px 24px; border-radius: 8px; font-size: 1rem;
|
|
159
|
+
font-weight: 600; cursor: pointer; border: none;
|
|
160
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
161
|
+
transition: all 0.15s; width: 100%; max-width: 320px;
|
|
162
|
+
-webkit-tap-highlight-color: transparent;
|
|
163
|
+
}
|
|
164
|
+
.wt-btn:active { transform: scale(0.97); }
|
|
165
|
+
.wt-btn-primary { background: ${accentColor}; color: #111; }
|
|
166
|
+
.wt-btn-secondary { background: transparent; color: #e0e0e0; border: 2px solid #444; }
|
|
167
|
+
.wt-btn-danger { background: #ff4444; color: #fff; }
|
|
168
|
+
.wt-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
|
|
169
|
+
|
|
170
|
+
.wt-btn-row { display: flex; gap: 10px; justify-content: center; margin-top: 12px; flex-wrap: wrap; }
|
|
171
|
+
|
|
172
|
+
/* Complete section */
|
|
173
|
+
.wt-complete-section {
|
|
174
|
+
text-align: center; margin-top: 32px; padding-top: 24px;
|
|
175
|
+
border-top: 1px solid #333;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Elapsed timer */
|
|
179
|
+
.wt-elapsed {
|
|
180
|
+
text-align: center; font-size: 0.85rem; color: #888;
|
|
181
|
+
margin-bottom: 20px; font-variant-numeric: tabular-nums;
|
|
182
|
+
}
|
|
183
|
+
.wt-elapsed-time { color: #e0e0e0; font-weight: 600; font-size: 1rem; }
|
|
184
|
+
|
|
185
|
+
/* Animations */
|
|
186
|
+
@keyframes wtFadeIn {
|
|
187
|
+
from { opacity: 0; transform: translateY(-8px); }
|
|
188
|
+
to { opacity: 1; transform: translateY(0); }
|
|
189
|
+
}
|
|
190
|
+
@keyframes wtPulse {
|
|
191
|
+
0%, 100% { box-shadow: 0 0 0 0 ${accentColor}40; }
|
|
192
|
+
50% { box-shadow: 0 0 0 8px ${accentColor}00; }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Mobile tweaks */
|
|
196
|
+
@media (max-width: 380px) {
|
|
197
|
+
.wt-stats { grid-template-columns: repeat(2, 1fr); }
|
|
198
|
+
.wt-rest-display { font-size: 2.8rem; }
|
|
199
|
+
}
|
|
200
|
+
</style>
|
|
201
|
+
|
|
202
|
+
<!-- Header -->
|
|
203
|
+
<div class="wt-header">
|
|
204
|
+
<h1>🏋️ ${esc(title)}</h1>
|
|
205
|
+
${subtitle ? `<p class="wt-sub">${esc(subtitle)}</p>` : ''}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- Stats Bar -->
|
|
209
|
+
<div class="wt-stats">
|
|
210
|
+
<div class="wt-stat">
|
|
211
|
+
<div class="wt-stat-icon">⏱️</div>
|
|
212
|
+
<div class="wt-stat-val">${estimatedMinutes || '—'}</div>
|
|
213
|
+
<div class="wt-stat-label">Minutes</div>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="wt-stat">
|
|
216
|
+
<div class="wt-stat-icon">💪</div>
|
|
217
|
+
<div class="wt-stat-val">${exercises.length}</div>
|
|
218
|
+
<div class="wt-stat-label">Exercises</div>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="wt-stat">
|
|
221
|
+
<div class="wt-stat-icon">🔥</div>
|
|
222
|
+
<div class="wt-stat-val">${estimatedCalories || '—'}</div>
|
|
223
|
+
<div class="wt-stat-label">Calories</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="wt-stat">
|
|
226
|
+
<div class="wt-stat-icon">🔄</div>
|
|
227
|
+
<div class="wt-stat-val">${rounds}</div>
|
|
228
|
+
<div class="wt-stat-label">Rounds</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<!-- Elapsed Timer -->
|
|
233
|
+
<div class="wt-elapsed">
|
|
234
|
+
Elapsed: <span class="wt-elapsed-time" id="wt_elapsed">00:00</span>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<!-- Warm-up Section -->
|
|
238
|
+
${warmup.length > 0 ? `
|
|
239
|
+
<div class="wt-section" id="wt_warmup">
|
|
240
|
+
<div class="wt-section-title">🔥 Warm-up</div>
|
|
241
|
+
${warmup.map((w, i) => `
|
|
242
|
+
<div class="wt-check-item" data-group="warmup" data-idx="${i}" onclick="wtToggleCheck(this)">
|
|
243
|
+
<div class="wt-check-circle">✓</div>
|
|
244
|
+
<span class="wt-check-text">${esc(w.text)}</span>
|
|
245
|
+
</div>`).join('')}
|
|
246
|
+
</div>` : ''}
|
|
247
|
+
|
|
248
|
+
<!-- Round Counter -->
|
|
249
|
+
<div class="wt-section">
|
|
250
|
+
<div class="wt-section-title">🏋️ Workout</div>
|
|
251
|
+
<div class="wt-round-bar" id="wt_round_bar">
|
|
252
|
+
${Array.from({ length: rounds }, (_, i) => `
|
|
253
|
+
<div class="wt-round-dot ${i === 0 ? 'active' : ''}" data-round="${i}" id="wt_rdot_${i}">${i + 1}</div>`).join('')}
|
|
254
|
+
<span class="wt-round-label" id="wt_round_label">Round 1 / ${rounds}</span>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<!-- Exercise Cards -->
|
|
258
|
+
<div id="wt_exercises">
|
|
259
|
+
${exercises.map((ex, i) => `
|
|
260
|
+
<div class="wt-exercise" data-ex="${i}">
|
|
261
|
+
<div class="wt-exercise-name">${esc(ex.name)}</div>
|
|
262
|
+
<div class="wt-exercise-reps">${esc(ex.reps)}</div>
|
|
263
|
+
${ex.notes ? `<div class="wt-exercise-notes">${esc(ex.notes)}</div>` : ''}
|
|
264
|
+
</div>`).join('')}
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<!-- Round Controls -->
|
|
268
|
+
<div class="wt-btn-row" id="wt_round_controls">
|
|
269
|
+
<button class="wt-btn wt-btn-primary" id="wt_next_round" onclick="wtNextRound()">
|
|
270
|
+
Complete Round → Start Rest
|
|
271
|
+
</button>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<!-- Rest Timer -->
|
|
276
|
+
<div class="wt-rest-panel" id="wt_rest_panel">
|
|
277
|
+
<div class="wt-rest-label">Rest Period</div>
|
|
278
|
+
<div class="wt-rest-display" id="wt_rest_display">${fmtTime(restSeconds)}</div>
|
|
279
|
+
<div class="wt-rest-progress">
|
|
280
|
+
<div class="wt-rest-bar" id="wt_rest_bar"></div>
|
|
281
|
+
</div>
|
|
282
|
+
<div class="wt-btn-row">
|
|
283
|
+
<button class="wt-btn wt-btn-secondary" id="wt_skip_rest" onclick="wtSkipRest()">Skip Rest</button>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<!-- Cool-down Section -->
|
|
288
|
+
${cooldown.length > 0 ? `
|
|
289
|
+
<div class="wt-section" id="wt_cooldown" style="display:none">
|
|
290
|
+
<div class="wt-section-title">🧘 Cool-down</div>
|
|
291
|
+
${cooldown.map((c, i) => `
|
|
292
|
+
<div class="wt-check-item" data-group="cooldown" data-idx="${i}" onclick="wtToggleCheck(this)">
|
|
293
|
+
<div class="wt-check-circle">✓</div>
|
|
294
|
+
<span class="wt-check-text">${esc(c.text)}</span>
|
|
295
|
+
</div>`).join('')}
|
|
296
|
+
</div>` : ''}
|
|
297
|
+
|
|
298
|
+
<!-- Complete Button -->
|
|
299
|
+
<div class="wt-complete-section" id="wt_complete_section" style="display:none">
|
|
300
|
+
<button class="wt-btn wt-btn-primary" id="wt_complete_btn" onclick="wtComplete()" style="animation: wtPulse 2s infinite">
|
|
301
|
+
✅ Complete Workout
|
|
302
|
+
</button>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<script>
|
|
306
|
+
(function() {
|
|
307
|
+
// ── State ──
|
|
308
|
+
var currentRound = 0;
|
|
309
|
+
var totalRounds = ${rounds};
|
|
310
|
+
var restDuration = ${restSeconds};
|
|
311
|
+
var restTimer = null;
|
|
312
|
+
var restRemaining = 0;
|
|
313
|
+
var elapsedTimer = null;
|
|
314
|
+
var elapsedSeconds = 0;
|
|
315
|
+
var startTime = null;
|
|
316
|
+
var exerciseCount = ${exercises.length};
|
|
317
|
+
var warmupCount = ${warmup.length};
|
|
318
|
+
var cooldownCount = ${cooldown.length};
|
|
319
|
+
var totalExercises = ${totalExercises};
|
|
320
|
+
var checkedItems = { warmup: {}, cooldown: {} };
|
|
321
|
+
var workoutTitle = ${JSON.stringify(title)};
|
|
322
|
+
|
|
323
|
+
// ── Elapsed timer ──
|
|
324
|
+
function startElapsed() {
|
|
325
|
+
if (elapsedTimer) return;
|
|
326
|
+
startTime = Date.now();
|
|
327
|
+
var el = document.getElementById('wt_elapsed');
|
|
328
|
+
elapsedTimer = setInterval(function() {
|
|
329
|
+
elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
|
|
330
|
+
el.textContent = fmtT(elapsedSeconds);
|
|
331
|
+
}, 1000);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function fmtT(s) {
|
|
335
|
+
var h = Math.floor(s / 3600);
|
|
336
|
+
var m = Math.floor((s % 3600) / 60);
|
|
337
|
+
var sec = s % 60;
|
|
338
|
+
if (h > 0) return h + ':' + (m < 10 ? '0' : '') + m + ':' + (sec < 10 ? '0' : '') + sec;
|
|
339
|
+
return (m < 10 ? '0' : '') + m + ':' + (sec < 10 ? '0' : '') + sec;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Checklist toggle ──
|
|
343
|
+
window.wtToggleCheck = function(el) {
|
|
344
|
+
startElapsed();
|
|
345
|
+
el.classList.toggle('checked');
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// ── Round management ──
|
|
349
|
+
function updateRoundDots() {
|
|
350
|
+
for (var i = 0; i < totalRounds; i++) {
|
|
351
|
+
var dot = document.getElementById('wt_rdot_' + i);
|
|
352
|
+
dot.className = 'wt-round-dot';
|
|
353
|
+
if (i < currentRound) dot.classList.add('done');
|
|
354
|
+
else if (i === currentRound) dot.classList.add('active');
|
|
355
|
+
}
|
|
356
|
+
var label = document.getElementById('wt_round_label');
|
|
357
|
+
if (currentRound < totalRounds) {
|
|
358
|
+
label.textContent = 'Round ' + (currentRound + 1) + ' / ' + totalRounds;
|
|
359
|
+
} else {
|
|
360
|
+
label.textContent = 'All rounds complete!';
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
window.wtNextRound = function() {
|
|
365
|
+
startElapsed();
|
|
366
|
+
currentRound++;
|
|
367
|
+
updateRoundDots();
|
|
368
|
+
|
|
369
|
+
if (currentRound >= totalRounds) {
|
|
370
|
+
// All rounds done — show cooldown or complete
|
|
371
|
+
document.getElementById('wt_round_controls').style.display = 'none';
|
|
372
|
+
document.getElementById('wt_rest_panel').className = 'wt-rest-panel';
|
|
373
|
+
var cd = document.getElementById('wt_cooldown');
|
|
374
|
+
if (cd) {
|
|
375
|
+
cd.style.display = 'block';
|
|
376
|
+
cd.style.animation = 'wtFadeIn 0.3s ease';
|
|
377
|
+
}
|
|
378
|
+
document.getElementById('wt_complete_section').style.display = 'block';
|
|
379
|
+
document.getElementById('wt_complete_section').style.animation = 'wtFadeIn 0.3s ease';
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Show rest timer
|
|
384
|
+
startRest();
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// ── Rest timer ──
|
|
388
|
+
function startRest() {
|
|
389
|
+
var panel = document.getElementById('wt_rest_panel');
|
|
390
|
+
var display = document.getElementById('wt_rest_display');
|
|
391
|
+
var bar = document.getElementById('wt_rest_bar');
|
|
392
|
+
|
|
393
|
+
panel.className = 'wt-rest-panel visible';
|
|
394
|
+
restRemaining = restDuration;
|
|
395
|
+
bar.style.transition = 'none';
|
|
396
|
+
bar.style.width = '0%';
|
|
397
|
+
|
|
398
|
+
// Scroll to rest panel
|
|
399
|
+
panel.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
400
|
+
|
|
401
|
+
clearInterval(restTimer);
|
|
402
|
+
restTimer = setInterval(function() {
|
|
403
|
+
restRemaining--;
|
|
404
|
+
display.textContent = fmtRestTime(restRemaining);
|
|
405
|
+
bar.style.transition = 'width 1s linear';
|
|
406
|
+
bar.style.width = (((restDuration - restRemaining) / restDuration) * 100) + '%';
|
|
407
|
+
|
|
408
|
+
if (restRemaining <= 0) {
|
|
409
|
+
clearInterval(restTimer);
|
|
410
|
+
restTimer = null;
|
|
411
|
+
endRest();
|
|
412
|
+
}
|
|
413
|
+
}, 1000);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function fmtRestTime(s) {
|
|
417
|
+
if (s < 0) s = 0;
|
|
418
|
+
var m = Math.floor(s / 60);
|
|
419
|
+
var sec = s % 60;
|
|
420
|
+
return (m < 10 ? '0' : '') + m + ':' + (sec < 10 ? '0' : '') + sec;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function endRest() {
|
|
424
|
+
var panel = document.getElementById('wt_rest_panel');
|
|
425
|
+
panel.className = 'wt-rest-panel';
|
|
426
|
+
|
|
427
|
+
// Update button text for next round
|
|
428
|
+
var btn = document.getElementById('wt_next_round');
|
|
429
|
+
if (currentRound >= totalRounds - 1) {
|
|
430
|
+
btn.textContent = 'Complete Final Round';
|
|
431
|
+
} else {
|
|
432
|
+
btn.textContent = 'Complete Round → Start Rest';
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Scroll exercises back into view
|
|
436
|
+
document.getElementById('wt_exercises').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
window.wtSkipRest = function() {
|
|
440
|
+
clearInterval(restTimer);
|
|
441
|
+
restTimer = null;
|
|
442
|
+
endRest();
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// ── Completion ──
|
|
446
|
+
window.wtComplete = function() {
|
|
447
|
+
if (elapsedTimer) { clearInterval(elapsedTimer); elapsedTimer = null; }
|
|
448
|
+
|
|
449
|
+
var warmupChecked = document.querySelectorAll('[data-group="warmup"].checked').length;
|
|
450
|
+
var cooldownChecked = document.querySelectorAll('[data-group="cooldown"].checked').length;
|
|
451
|
+
var exercisesChecked = (currentRound * exerciseCount) + warmupChecked + cooldownChecked;
|
|
452
|
+
|
|
453
|
+
var payload = {
|
|
454
|
+
action: 'workout_complete',
|
|
455
|
+
title: workoutTitle,
|
|
456
|
+
roundsCompleted: currentRound,
|
|
457
|
+
exercisesChecked: exercisesChecked,
|
|
458
|
+
totalExercises: totalExercises,
|
|
459
|
+
duration: elapsedSeconds,
|
|
460
|
+
completedAt: new Date().toISOString()
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// Visual feedback
|
|
464
|
+
var btn = document.getElementById('wt_complete_btn');
|
|
465
|
+
btn.textContent = '🎉 Workout Complete!';
|
|
466
|
+
btn.disabled = true;
|
|
467
|
+
btn.style.animation = 'none';
|
|
468
|
+
btn.style.background = '#444';
|
|
469
|
+
btn.style.color = '#e0e0e0';
|
|
470
|
+
|
|
471
|
+
if (window.sparkui) {
|
|
472
|
+
window.sparkui.send('completion', payload);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// ── Init ──
|
|
477
|
+
updateRoundDots();
|
|
478
|
+
var nextBtn = document.getElementById('wt_next_round');
|
|
479
|
+
if (totalRounds <= 1) {
|
|
480
|
+
nextBtn.textContent = 'Complete Final Round';
|
|
481
|
+
}
|
|
482
|
+
})();
|
|
483
|
+
</script>`;
|
|
484
|
+
|
|
485
|
+
return base({
|
|
486
|
+
title: title,
|
|
487
|
+
body: body,
|
|
488
|
+
id: _pageId,
|
|
489
|
+
og: {
|
|
490
|
+
title: _og.title || title,
|
|
491
|
+
description: _og.description || 'Workout powered by SparkUI',
|
|
492
|
+
image: _og.image || '',
|
|
493
|
+
url: _og.url || '',
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ── Utility ──
|
|
499
|
+
function esc(str) {
|
|
500
|
+
if (!str) return '';
|
|
501
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function fmtTime(s) {
|
|
505
|
+
var m = Math.floor(s / 60);
|
|
506
|
+
var sec = s % 60;
|
|
507
|
+
return (m < 10 ? '0' : '') + m + ':' + (sec < 10 ? '0' : '') + sec;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
module.exports = workoutTimer;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const base = require('./base');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WebSocket test/demo template.
|
|
7
|
+
* Tests all WS bridge features: events, completion, status, server messages.
|
|
8
|
+
*/
|
|
9
|
+
function wsTest(data = {}) {
|
|
10
|
+
const pageId = data._pageId || 'unknown';
|
|
11
|
+
const _og = data._og || {};
|
|
12
|
+
|
|
13
|
+
const body = `
|
|
14
|
+
<h1 style="font-size:1.5rem;margin-bottom:8px">⚡ SparkUI WebSocket Test</h1>
|
|
15
|
+
<p style="color:#888;margin-bottom:24px">Page ID: <code style="color:#6cf">${pageId}</code></p>
|
|
16
|
+
|
|
17
|
+
<!-- Connection Status -->
|
|
18
|
+
<div id="status" style="padding:12px;border-radius:8px;margin-bottom:20px;background:#1a1a1a;border:1px solid #333">
|
|
19
|
+
<span id="status-dot" style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#f44;margin-right:8px;vertical-align:middle"></span>
|
|
20
|
+
<span id="status-text" style="vertical-align:middle">Connecting...</span>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- Test Buttons -->
|
|
24
|
+
<div style="display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px">
|
|
25
|
+
<button id="btn-click" style="padding:10px 20px;border-radius:6px;border:none;background:#2563eb;color:#fff;font-size:1rem;cursor:pointer;flex:1;min-width:120px">
|
|
26
|
+
🖱️ Send Click Event
|
|
27
|
+
</button>
|
|
28
|
+
<button id="btn-custom" style="padding:10px 20px;border-radius:6px;border:none;background:#7c3aed;color:#fff;font-size:1rem;cursor:pointer;flex:1;min-width:120px">
|
|
29
|
+
⚡ Send Custom Event
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<!-- Form -->
|
|
34
|
+
<form id="test-form" style="background:#1a1a1a;padding:16px;border-radius:8px;margin-bottom:20px;border:1px solid #333">
|
|
35
|
+
<h3 style="margin-bottom:12px;font-size:1rem">📋 Test Form (sends completion)</h3>
|
|
36
|
+
<label style="display:block;margin-bottom:8px;color:#aaa;font-size:0.9rem">Name</label>
|
|
37
|
+
<input id="form-name" type="text" placeholder="Enter name" style="width:100%;padding:8px 12px;border-radius:4px;border:1px solid #444;background:#222;color:#eee;font-size:1rem;margin-bottom:12px">
|
|
38
|
+
<label style="display:block;margin-bottom:8px;color:#aaa;font-size:0.9rem">Rating</label>
|
|
39
|
+
<select id="form-rating" style="width:100%;padding:8px 12px;border-radius:4px;border:1px solid #444;background:#222;color:#eee;font-size:1rem;margin-bottom:16px">
|
|
40
|
+
<option value="5">⭐⭐⭐⭐⭐ Excellent</option>
|
|
41
|
+
<option value="4">⭐⭐⭐⭐ Good</option>
|
|
42
|
+
<option value="3">⭐⭐⭐ OK</option>
|
|
43
|
+
<option value="2">⭐⭐ Poor</option>
|
|
44
|
+
<option value="1">⭐ Bad</option>
|
|
45
|
+
</select>
|
|
46
|
+
<button type="submit" style="width:100%;padding:10px;border-radius:6px;border:none;background:#059669;color:#fff;font-size:1rem;cursor:pointer">
|
|
47
|
+
✅ Submit (sends completion)
|
|
48
|
+
</button>
|
|
49
|
+
</form>
|
|
50
|
+
|
|
51
|
+
<!-- Message Log -->
|
|
52
|
+
<div style="background:#1a1a1a;padding:16px;border-radius:8px;border:1px solid #333">
|
|
53
|
+
<h3 style="margin-bottom:12px;font-size:1rem">📨 Message Log</h3>
|
|
54
|
+
<div id="log" style="font-family:monospace;font-size:0.85rem;max-height:300px;overflow-y:auto">
|
|
55
|
+
<div style="color:#555">Waiting for messages...</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const extraHead = `
|
|
61
|
+
<script>
|
|
62
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
63
|
+
var log = document.getElementById('log');
|
|
64
|
+
var statusDot = document.getElementById('status-dot');
|
|
65
|
+
var statusText = document.getElementById('status-text');
|
|
66
|
+
|
|
67
|
+
function addLog(msg, color) {
|
|
68
|
+
var div = document.createElement('div');
|
|
69
|
+
div.style.cssText = 'padding:4px 0;border-bottom:1px solid #222;color:' + (color || '#ccc');
|
|
70
|
+
var time = new Date().toLocaleTimeString();
|
|
71
|
+
div.textContent = '[' + time + '] ' + msg;
|
|
72
|
+
if (log.firstChild && log.firstChild.textContent === 'Waiting for messages...') {
|
|
73
|
+
log.innerHTML = '';
|
|
74
|
+
}
|
|
75
|
+
log.appendChild(div);
|
|
76
|
+
log.scrollTop = log.scrollHeight;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Connection status
|
|
80
|
+
document.addEventListener('sparkui:status', function(e) {
|
|
81
|
+
var s = e.detail.status;
|
|
82
|
+
statusText.textContent = s.charAt(0).toUpperCase() + s.slice(1);
|
|
83
|
+
statusDot.style.background = s === 'connected' ? '#4ade80' : '#f44';
|
|
84
|
+
addLog('Status: ' + s, s === 'connected' ? '#4ade80' : '#f44');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Listen for server messages
|
|
88
|
+
if (window.sparkui) {
|
|
89
|
+
sparkui.onMessage(function(msg) {
|
|
90
|
+
addLog('← Server: ' + JSON.stringify(msg), '#6cf');
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Button: click event
|
|
95
|
+
document.getElementById('btn-click').addEventListener('click', function() {
|
|
96
|
+
sparkui.send('event', { action: 'click', button: 'test', timestamp: Date.now() });
|
|
97
|
+
addLog('→ Sent click event', '#fbbf24');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Button: custom event
|
|
101
|
+
document.getElementById('btn-custom').addEventListener('click', function() {
|
|
102
|
+
sparkui.send('event', { action: 'custom', value: Math.random().toString(36).slice(2, 8) });
|
|
103
|
+
addLog('→ Sent custom event', '#c084fc');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Form: completion
|
|
107
|
+
document.getElementById('test-form').addEventListener('submit', function(e) {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
var formData = {
|
|
110
|
+
name: document.getElementById('form-name').value,
|
|
111
|
+
rating: document.getElementById('form-rating').value,
|
|
112
|
+
};
|
|
113
|
+
sparkui.sendCompletion({ formData: formData, completedAt: Date.now() });
|
|
114
|
+
addLog('→ Sent completion: ' + JSON.stringify(formData), '#34d399');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
</script>
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
const og = {
|
|
121
|
+
title: _og.title || 'WebSocket Test',
|
|
122
|
+
description: _og.description || 'SparkUI WebSocket connectivity test ⚡',
|
|
123
|
+
image: _og.image,
|
|
124
|
+
url: _og.url,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return base({
|
|
128
|
+
title: 'SparkUI WS Test',
|
|
129
|
+
body,
|
|
130
|
+
id: pageId,
|
|
131
|
+
extraHead,
|
|
132
|
+
og,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = wsTest;
|