vibespot 0.4.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/LICENSE +33 -0
- package/README.md +118 -0
- package/assets/content-guide.md +445 -0
- package/assets/conversion-guide.md +693 -0
- package/assets/design-guide.md +380 -0
- package/assets/hubspot-rules.md +560 -0
- package/assets/page-types.md +116 -0
- package/bin/vibespot.mjs +11 -0
- package/dist/index.js +6552 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
- package/ui/chat.js +803 -0
- package/ui/dashboard.js +383 -0
- package/ui/dialog.js +117 -0
- package/ui/field-editor.js +292 -0
- package/ui/index.html +393 -0
- package/ui/preview.js +132 -0
- package/ui/settings.js +927 -0
- package/ui/setup.js +830 -0
- package/ui/styles.css +2552 -0
- package/ui/upload-panel.js +554 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload panel — slide-up log for live hs upload output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
let uploadState = "idle";
|
|
6
|
+
let uploadAttempt = 0;
|
|
7
|
+
let lastUploadErrors = [];
|
|
8
|
+
let lastUploadPortalId = "";
|
|
9
|
+
let lastUploadDataCenter = "na1";
|
|
10
|
+
let lastUploadThemeName = "";
|
|
11
|
+
const MAX_UPLOAD_ATTEMPTS = 3;
|
|
12
|
+
|
|
13
|
+
async function startUpload() {
|
|
14
|
+
if (uploadState === "uploading" || uploadState === "ai_fixing") return;
|
|
15
|
+
|
|
16
|
+
const uploadBtn = document.getElementById("btn-upload");
|
|
17
|
+
if (uploadBtn) {
|
|
18
|
+
uploadBtn.innerHTML = '<span class="upload-spinner"></span>';
|
|
19
|
+
uploadBtn.disabled = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resetUploadBtn() {
|
|
23
|
+
if (uploadBtn) { uploadBtn.textContent = "Deploy"; uploadBtn.disabled = false; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Fetch portal info and check HubSpot CLI readiness
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch("/api/settings/status");
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
let hs = data.environment?.tools?.hubspot;
|
|
31
|
+
|
|
32
|
+
// HubSpot CLI not installed — guide the user through install
|
|
33
|
+
if (!hs || !hs.found) {
|
|
34
|
+
const installed = await showHubSpotSetupDialog("install");
|
|
35
|
+
if (!installed) { resetUploadBtn(); return; }
|
|
36
|
+
// Re-check after install
|
|
37
|
+
const recheck = await fetch("/api/settings/status").then((r) => r.json());
|
|
38
|
+
hs = recheck.environment?.tools?.hubspot;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// HubSpot CLI installed but not authenticated — guide through auth
|
|
42
|
+
if (hs && hs.found && !hs.authenticated) {
|
|
43
|
+
const authed = await showHubSpotSetupDialog("auth");
|
|
44
|
+
if (!authed) { resetUploadBtn(); return; }
|
|
45
|
+
// Re-check after auth
|
|
46
|
+
const recheck = await fetch("/api/settings/status").then((r) => r.json());
|
|
47
|
+
hs = recheck.environment?.tools?.hubspot;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Confirm portal before deploying
|
|
51
|
+
if (hs && hs.authenticated && hs.portalName) {
|
|
52
|
+
const confirmed = await confirmUpload(hs.portalName, hs.portalId);
|
|
53
|
+
if (!confirmed) { resetUploadBtn(); return; }
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// If we can't detect, proceed and let the upload fail with a meaningful error
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
doUpload();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Show a dialog to guide the user through HubSpot CLI install or auth.
|
|
64
|
+
* @param {"install" | "auth"} mode
|
|
65
|
+
* @returns {Promise<boolean>} true if the step completed successfully
|
|
66
|
+
*/
|
|
67
|
+
function showHubSpotSetupDialog(mode) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const overlay = document.createElement("div");
|
|
70
|
+
overlay.className = "confirm-overlay";
|
|
71
|
+
|
|
72
|
+
if (mode === "install") {
|
|
73
|
+
overlay.innerHTML = `
|
|
74
|
+
<div class="confirm-dialog" style="width:420px">
|
|
75
|
+
<div class="confirm-dialog__title">Install HubSpot CLI</div>
|
|
76
|
+
<p class="confirm-dialog__detail">The HubSpot CLI is needed to deploy your theme. This will run <code>npm install -g @hubspot/cli</code>.</p>
|
|
77
|
+
<div class="confirm-dialog__actions">
|
|
78
|
+
<button class="btn btn--secondary" data-action="cancel">Cancel</button>
|
|
79
|
+
<button class="btn btn--primary" data-action="install">Install</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="hs-setup__status hidden" id="hs-install-status">
|
|
82
|
+
<span class="upload-spinner"></span> Installing...
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
`;
|
|
86
|
+
document.body.appendChild(overlay);
|
|
87
|
+
|
|
88
|
+
const close = (val) => { overlay.remove(); resolve(val); };
|
|
89
|
+
overlay.querySelector('[data-action="cancel"]').addEventListener("click", () => close(false));
|
|
90
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(false); });
|
|
91
|
+
|
|
92
|
+
overlay.querySelector('[data-action="install"]').addEventListener("click", async () => {
|
|
93
|
+
const installBtn = overlay.querySelector('[data-action="install"]');
|
|
94
|
+
installBtn.disabled = true;
|
|
95
|
+
installBtn.textContent = "Installing...";
|
|
96
|
+
const statusEl = document.getElementById("hs-install-status");
|
|
97
|
+
if (statusEl) statusEl.classList.remove("hidden");
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch("/api/settings/install", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body: JSON.stringify({ tool: "hubspot" }),
|
|
104
|
+
});
|
|
105
|
+
const data = await res.json();
|
|
106
|
+
if (data.jobId) {
|
|
107
|
+
// Poll until done
|
|
108
|
+
for (let i = 0; i < 60; i++) {
|
|
109
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
110
|
+
const jr = await fetch("/api/settings/job/" + data.jobId).then((r) => r.json());
|
|
111
|
+
if (jr.status === "completed") { close(true); return; }
|
|
112
|
+
if (jr.status === "failed") { close(false); return; }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
close(false);
|
|
116
|
+
} catch {
|
|
117
|
+
close(false);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
} else {
|
|
122
|
+
// Auth mode — PAK flow
|
|
123
|
+
overlay.innerHTML = `
|
|
124
|
+
<div class="confirm-dialog" style="width:460px">
|
|
125
|
+
<div class="confirm-dialog__title">Connect your HubSpot account</div>
|
|
126
|
+
<p class="confirm-dialog__detail">Create a Personal Access Key to connect vibeSpot to your HubSpot portal.</p>
|
|
127
|
+
<ol class="hs-setup__steps">
|
|
128
|
+
<li>Open <a href="https://app.hubspot.com/portal-recommend/l?slug=personal-access-key" target="_blank" rel="noopener">HubSpot Settings</a></li>
|
|
129
|
+
<li>Click "Create personal access key" or copy an existing one</li>
|
|
130
|
+
<li>Paste the key below</li>
|
|
131
|
+
</ol>
|
|
132
|
+
<input type="password" class="confirm-dialog__input" id="hs-pak-input" placeholder="pat-na1-..." />
|
|
133
|
+
<div class="confirm-dialog__actions">
|
|
134
|
+
<button class="btn btn--secondary" data-action="cancel">Cancel</button>
|
|
135
|
+
<button class="btn btn--primary" data-action="save">Connect</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
`;
|
|
139
|
+
document.body.appendChild(overlay);
|
|
140
|
+
|
|
141
|
+
const close = (val) => { overlay.remove(); resolve(val); };
|
|
142
|
+
overlay.querySelector('[data-action="cancel"]').addEventListener("click", () => close(false));
|
|
143
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(false); });
|
|
144
|
+
|
|
145
|
+
const pakInput = document.getElementById("hs-pak-input");
|
|
146
|
+
const saveBtn = overlay.querySelector('[data-action="save"]');
|
|
147
|
+
|
|
148
|
+
const doSave = async () => {
|
|
149
|
+
const key = pakInput.value.trim();
|
|
150
|
+
if (!key) return;
|
|
151
|
+
saveBtn.disabled = true;
|
|
152
|
+
saveBtn.textContent = "Connecting...";
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetch("/api/settings/hs-auth", {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: { "Content-Type": "application/json" },
|
|
157
|
+
body: JSON.stringify({ personalAccessKey: key }),
|
|
158
|
+
});
|
|
159
|
+
const data = await res.json();
|
|
160
|
+
if (data.error) {
|
|
161
|
+
await vibeAlert(data.error, "Error");
|
|
162
|
+
saveBtn.disabled = false;
|
|
163
|
+
saveBtn.textContent = "Connect";
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
close(true);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
await vibeAlert("Failed to connect: " + err.message, "Error");
|
|
169
|
+
saveBtn.disabled = false;
|
|
170
|
+
saveBtn.textContent = "Connect";
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
saveBtn.addEventListener("click", doSave);
|
|
175
|
+
pakInput.addEventListener("keydown", (e) => { if (e.key === "Enter") doSave(); });
|
|
176
|
+
setTimeout(() => pakInput.focus(), 50);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function doUpload() {
|
|
182
|
+
uploadAttempt = 0;
|
|
183
|
+
const panel = document.getElementById("upload-panel");
|
|
184
|
+
const log = document.getElementById("upload-log");
|
|
185
|
+
log.textContent = "";
|
|
186
|
+
panel.classList.remove("hidden");
|
|
187
|
+
panel.className = "upload-panel";
|
|
188
|
+
|
|
189
|
+
setUploadState("writing");
|
|
190
|
+
|
|
191
|
+
// Send via WebSocket
|
|
192
|
+
if (typeof ws !== "undefined" && ws.readyState === WebSocket.OPEN) {
|
|
193
|
+
ws.send(JSON.stringify({ type: "start_upload" }));
|
|
194
|
+
} else {
|
|
195
|
+
appendUploadLog("Error: Not connected to server\n");
|
|
196
|
+
setUploadState("failed");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function confirmUpload(portalName, portalId) {
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
const overlay = document.createElement("div");
|
|
203
|
+
overlay.className = "confirm-overlay";
|
|
204
|
+
overlay.innerHTML = `
|
|
205
|
+
<div class="confirm-dialog">
|
|
206
|
+
<div class="confirm-dialog__title">Deploy to HubSpot?</div>
|
|
207
|
+
<p class="confirm-dialog__detail">
|
|
208
|
+
Uploading to <strong>${esc(portalName)}</strong>${portalId ? ` (${esc(portalId)})` : ""}
|
|
209
|
+
</p>
|
|
210
|
+
<div class="confirm-dialog__actions">
|
|
211
|
+
<button class="btn btn--secondary" id="confirm-upload-cancel">Cancel</button>
|
|
212
|
+
<button class="btn btn--primary" id="confirm-upload-go">Deploy</button>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
`;
|
|
216
|
+
document.body.appendChild(overlay);
|
|
217
|
+
|
|
218
|
+
document.getElementById("confirm-upload-cancel").addEventListener("click", () => {
|
|
219
|
+
overlay.remove();
|
|
220
|
+
resolve(false);
|
|
221
|
+
});
|
|
222
|
+
overlay.addEventListener("click", (e) => {
|
|
223
|
+
if (e.target === overlay) { overlay.remove(); resolve(false); }
|
|
224
|
+
});
|
|
225
|
+
document.getElementById("confirm-upload-go").addEventListener("click", () => {
|
|
226
|
+
overlay.remove();
|
|
227
|
+
resolve(true);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function setUploadState(state, data) {
|
|
233
|
+
uploadState = state;
|
|
234
|
+
const panel = document.getElementById("upload-panel");
|
|
235
|
+
const statusEl = document.getElementById("upload-status-text");
|
|
236
|
+
const actions = document.getElementById("upload-actions");
|
|
237
|
+
const uploadBtn = document.getElementById("btn-upload");
|
|
238
|
+
|
|
239
|
+
// Reset panel state classes
|
|
240
|
+
panel.className = "upload-panel";
|
|
241
|
+
actions.innerHTML = "";
|
|
242
|
+
|
|
243
|
+
switch (state) {
|
|
244
|
+
case "idle":
|
|
245
|
+
panel.classList.add("hidden");
|
|
246
|
+
if (uploadBtn) {
|
|
247
|
+
uploadBtn.textContent = "Deploy";
|
|
248
|
+
uploadBtn.disabled = false;
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
case "writing":
|
|
253
|
+
statusEl.textContent = "Writing files to disk...";
|
|
254
|
+
if (uploadBtn) {
|
|
255
|
+
uploadBtn.innerHTML = '<span class="upload-spinner"></span> Preparing...';
|
|
256
|
+
uploadBtn.disabled = true;
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case "autofix":
|
|
261
|
+
statusEl.textContent = "Applying auto-fixes...";
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case "uploading":
|
|
265
|
+
panel.classList.add("upload-panel--uploading");
|
|
266
|
+
statusEl.textContent = "Uploading to HubSpot" + (uploadAttempt > 1 ? ` (attempt ${uploadAttempt})` : "") + "...";
|
|
267
|
+
if (uploadBtn) {
|
|
268
|
+
uploadBtn.innerHTML = '<span class="upload-spinner"></span> Uploading...';
|
|
269
|
+
uploadBtn.disabled = true;
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
case "success":
|
|
274
|
+
panel.classList.add("upload-panel--success");
|
|
275
|
+
statusEl.innerHTML = '<span class="upload-status-icon">✓</span> Upload complete!';
|
|
276
|
+
if (uploadBtn) {
|
|
277
|
+
uploadBtn.textContent = "Deploy";
|
|
278
|
+
uploadBtn.disabled = false;
|
|
279
|
+
}
|
|
280
|
+
const dismissBtn = document.createElement("button");
|
|
281
|
+
dismissBtn.className = "upload-action-btn";
|
|
282
|
+
dismissBtn.textContent = "Dismiss";
|
|
283
|
+
dismissBtn.addEventListener("click", () => setUploadState("idle"));
|
|
284
|
+
actions.appendChild(dismissBtn);
|
|
285
|
+
|
|
286
|
+
// Show the Create Page button
|
|
287
|
+
if (lastUploadPortalId) {
|
|
288
|
+
const createBtn = document.createElement("button");
|
|
289
|
+
createBtn.className = "upload-action-btn upload-action-btn--primary";
|
|
290
|
+
createBtn.textContent = "Create Page in HubSpot";
|
|
291
|
+
createBtn.addEventListener("click", () => {
|
|
292
|
+
window.open(buildHubSpotPagesUrl(lastUploadPortalId, lastUploadDataCenter), "_blank");
|
|
293
|
+
});
|
|
294
|
+
actions.insertBefore(createBtn, dismissBtn);
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
|
|
298
|
+
case "failed":
|
|
299
|
+
panel.classList.add("upload-panel--error");
|
|
300
|
+
statusEl.innerHTML = '<span class="upload-status-icon">✗</span> Upload failed';
|
|
301
|
+
if (uploadBtn) {
|
|
302
|
+
uploadBtn.textContent = "Deploy";
|
|
303
|
+
uploadBtn.disabled = false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const retryBtn = document.createElement("button");
|
|
307
|
+
retryBtn.className = "upload-action-btn";
|
|
308
|
+
retryBtn.textContent = "Retry";
|
|
309
|
+
retryBtn.addEventListener("click", () => startUpload());
|
|
310
|
+
actions.appendChild(retryBtn);
|
|
311
|
+
|
|
312
|
+
const fixBtn = document.createElement("button");
|
|
313
|
+
fixBtn.className = "upload-action-btn upload-action-btn--primary";
|
|
314
|
+
fixBtn.textContent = "Fix with AI";
|
|
315
|
+
fixBtn.addEventListener("click", () => fixUploadWithAI());
|
|
316
|
+
actions.appendChild(fixBtn);
|
|
317
|
+
|
|
318
|
+
const dismissFailBtn = document.createElement("button");
|
|
319
|
+
dismissFailBtn.className = "upload-action-btn";
|
|
320
|
+
dismissFailBtn.textContent = "Dismiss";
|
|
321
|
+
dismissFailBtn.addEventListener("click", () => setUploadState("idle"));
|
|
322
|
+
actions.appendChild(dismissFailBtn);
|
|
323
|
+
break;
|
|
324
|
+
|
|
325
|
+
case "ai_fixing":
|
|
326
|
+
panel.classList.add("upload-panel--fixing");
|
|
327
|
+
statusEl.innerHTML = '<span class="upload-spinner"></span> AI is fixing errors...';
|
|
328
|
+
if (uploadBtn) {
|
|
329
|
+
uploadBtn.innerHTML = '<span class="upload-spinner"></span> Fixing...';
|
|
330
|
+
uploadBtn.disabled = true;
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
|
|
334
|
+
case "fix_done":
|
|
335
|
+
panel.classList.add("upload-panel--fixing");
|
|
336
|
+
statusEl.innerHTML = '<span class="upload-status-icon">✓</span> AI fixes applied';
|
|
337
|
+
if (uploadBtn) {
|
|
338
|
+
uploadBtn.textContent = "Deploy";
|
|
339
|
+
uploadBtn.disabled = false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const redeployBtn = document.createElement("button");
|
|
343
|
+
redeployBtn.className = "upload-action-btn upload-action-btn--primary";
|
|
344
|
+
redeployBtn.textContent = "Re-deploy";
|
|
345
|
+
redeployBtn.addEventListener("click", () => startUpload());
|
|
346
|
+
actions.appendChild(redeployBtn);
|
|
347
|
+
|
|
348
|
+
const dismissFixBtn = document.createElement("button");
|
|
349
|
+
dismissFixBtn.className = "upload-action-btn";
|
|
350
|
+
dismissFixBtn.textContent = "Dismiss";
|
|
351
|
+
dismissFixBtn.addEventListener("click", () => setUploadState("idle"));
|
|
352
|
+
actions.appendChild(dismissFixBtn);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function appendUploadLog(text) {
|
|
358
|
+
const log = document.getElementById("upload-log");
|
|
359
|
+
if (!log) return;
|
|
360
|
+
log.textContent += text;
|
|
361
|
+
log.scrollTop = log.scrollHeight;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function fixUploadWithAI() {
|
|
365
|
+
const log = document.getElementById("upload-log");
|
|
366
|
+
const errorContext = log ? log.textContent.slice(-2000) : "";
|
|
367
|
+
|
|
368
|
+
// Show a verbose summary of what's happening
|
|
369
|
+
appendUploadLog("\n\n========================================\n");
|
|
370
|
+
appendUploadLog(" AI FIX — Analyzing upload errors\n");
|
|
371
|
+
appendUploadLog("========================================\n\n");
|
|
372
|
+
|
|
373
|
+
if (lastUploadErrors.length > 0) {
|
|
374
|
+
appendUploadLog("Errors detected:\n");
|
|
375
|
+
lastUploadErrors.forEach((e, i) => {
|
|
376
|
+
appendUploadLog(` ${i + 1}. ${e.message}\n`);
|
|
377
|
+
appendUploadLog(` File: ${e.file}\n`);
|
|
378
|
+
if (e.fixable) {
|
|
379
|
+
appendUploadLog(" Status: Auto-fixable — AI will patch this\n");
|
|
380
|
+
} else {
|
|
381
|
+
appendUploadLog(" Status: Requires AI analysis\n");
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
appendUploadLog("\n");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
appendUploadLog("Sending errors to AI for diagnosis and repair...\n\n");
|
|
388
|
+
|
|
389
|
+
setUploadState("ai_fixing");
|
|
390
|
+
|
|
391
|
+
if (typeof ws !== "undefined" && ws.readyState === WebSocket.OPEN) {
|
|
392
|
+
ws.send(JSON.stringify({ type: "upload_fix_with_ai", errorContext }));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function handleUploadWsMessage(msg) {
|
|
397
|
+
switch (msg.type) {
|
|
398
|
+
case "upload_status":
|
|
399
|
+
if (msg.phase === "autofix" && msg.fixes?.length > 0) {
|
|
400
|
+
setUploadState("autofix");
|
|
401
|
+
appendUploadLog("Auto-fixes applied:\n");
|
|
402
|
+
msg.fixes.forEach((f) => appendUploadLog(" \u2713 " + f + "\n"));
|
|
403
|
+
appendUploadLog("\n");
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
|
|
407
|
+
case "upload_started":
|
|
408
|
+
uploadAttempt++;
|
|
409
|
+
setUploadState("uploading");
|
|
410
|
+
break;
|
|
411
|
+
|
|
412
|
+
case "upload_output":
|
|
413
|
+
appendUploadLog(msg.chunk);
|
|
414
|
+
break;
|
|
415
|
+
|
|
416
|
+
case "upload_complete":
|
|
417
|
+
setUploadState("success");
|
|
418
|
+
// Update status bar
|
|
419
|
+
const statusText = document.getElementById("status-text");
|
|
420
|
+
if (statusText) statusText.textContent = "Upload complete!";
|
|
421
|
+
// Show celebration popup
|
|
422
|
+
showDeploySuccessPopup(msg.portalId, msg.dataCenter || "na1", msg.themeName);
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
case "upload_failed":
|
|
426
|
+
lastUploadErrors = msg.errors || [];
|
|
427
|
+
setUploadState("failed", { errors: msg.errors });
|
|
428
|
+
appendUploadLog("\n--- Upload failed ---\n\n");
|
|
429
|
+
if (msg.errors && msg.errors.length > 0) {
|
|
430
|
+
appendUploadLog(`${msg.errors.length} error(s) found:\n`);
|
|
431
|
+
msg.errors.forEach((e, i) => {
|
|
432
|
+
appendUploadLog(` ${i + 1}. \u2717 ${e.message}\n`);
|
|
433
|
+
appendUploadLog(` File: ${e.file}\n`);
|
|
434
|
+
});
|
|
435
|
+
} else {
|
|
436
|
+
appendUploadLog(" Check the log above for details.\n");
|
|
437
|
+
}
|
|
438
|
+
appendUploadLog("\nClick \"Fix with AI\" to automatically diagnose and repair these issues.\n");
|
|
439
|
+
break;
|
|
440
|
+
|
|
441
|
+
case "upload_fix_started":
|
|
442
|
+
setUploadState("ai_fixing");
|
|
443
|
+
break;
|
|
444
|
+
|
|
445
|
+
case "upload_fix_stream":
|
|
446
|
+
// AI fix explanation streamed into the upload panel
|
|
447
|
+
appendUploadLog(msg.content || "");
|
|
448
|
+
break;
|
|
449
|
+
|
|
450
|
+
case "upload_fix_complete":
|
|
451
|
+
appendUploadLog("\n\n========================================\n");
|
|
452
|
+
appendUploadLog(" Fix complete\n");
|
|
453
|
+
appendUploadLog("========================================\n");
|
|
454
|
+
lastUploadErrors = [];
|
|
455
|
+
setUploadState("fix_done");
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function buildHubSpotPagesUrl(portalId, dataCenter) {
|
|
461
|
+
const host = dataCenter === "eu1" ? "app-eu1.hubspot.com" : "app.hubspot.com";
|
|
462
|
+
return `https://${host}/page-ui/${portalId}/management/pages/landing`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function showDeploySuccessPopup(portalId, dataCenter, themeName) {
|
|
466
|
+
lastUploadPortalId = portalId || "";
|
|
467
|
+
lastUploadDataCenter = dataCenter || "na1";
|
|
468
|
+
lastUploadThemeName = themeName || "";
|
|
469
|
+
|
|
470
|
+
const name = themeName || "your theme";
|
|
471
|
+
const lpLabel = `${name} Landing Page`;
|
|
472
|
+
const pagesUrl = portalId ? buildHubSpotPagesUrl(portalId, dataCenter) : "";
|
|
473
|
+
|
|
474
|
+
// Spawn confetti
|
|
475
|
+
spawnConfetti();
|
|
476
|
+
|
|
477
|
+
const overlay = document.createElement("div");
|
|
478
|
+
overlay.className = "confirm-overlay";
|
|
479
|
+
overlay.innerHTML = `
|
|
480
|
+
<div class="deploy-success">
|
|
481
|
+
<div class="deploy-success__icon">🎉</div>
|
|
482
|
+
<h2 class="deploy-success__title">Theme deployed!</h2>
|
|
483
|
+
<p class="deploy-success__subtitle">"${esc(name)}" is now live on HubSpot.</p>
|
|
484
|
+
<div class="deploy-success__steps">
|
|
485
|
+
<div class="deploy-success__step"><span class="deploy-success__num">1</span> Go to <strong>Content → Landing Pages</strong></div>
|
|
486
|
+
<div class="deploy-success__step"><span class="deploy-success__num">2</span> Click <strong>"Create" → "Landing page"</strong></div>
|
|
487
|
+
<div class="deploy-success__step"><span class="deploy-success__num">3</span> Select the <strong>"${esc(lpLabel)}"</strong> template</div>
|
|
488
|
+
</div>
|
|
489
|
+
<div class="deploy-success__actions">
|
|
490
|
+
${pagesUrl ? `<a href="${pagesUrl}" target="_blank" class="btn btn--primary deploy-success__link">Create Page in HubSpot →</a>` : ""}
|
|
491
|
+
<button class="btn btn--secondary" id="deploy-success-dismiss">Close</button>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
`;
|
|
495
|
+
document.body.appendChild(overlay);
|
|
496
|
+
|
|
497
|
+
document.getElementById("deploy-success-dismiss").addEventListener("click", () => overlay.remove());
|
|
498
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function esc(str) {
|
|
502
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function spawnConfetti() {
|
|
506
|
+
const colors = ["#e8613a", "#f2825f", "#4ade80", "#f59e0b", "#818cf8", "#fb7185"];
|
|
507
|
+
const container = document.createElement("div");
|
|
508
|
+
container.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:10000;overflow:hidden";
|
|
509
|
+
document.body.appendChild(container);
|
|
510
|
+
|
|
511
|
+
for (let i = 0; i < 60; i++) {
|
|
512
|
+
const piece = document.createElement("div");
|
|
513
|
+
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
514
|
+
const left = Math.random() * 100;
|
|
515
|
+
const delay = Math.random() * 0.6;
|
|
516
|
+
const size = 6 + Math.random() * 6;
|
|
517
|
+
const drift = (Math.random() - 0.5) * 120;
|
|
518
|
+
piece.style.cssText = `
|
|
519
|
+
position:absolute;top:-10px;left:${left}%;
|
|
520
|
+
width:${size}px;height:${size * 0.6}px;
|
|
521
|
+
background:${color};border-radius:2px;
|
|
522
|
+
animation:confettiFall ${1.8 + Math.random() * 1.2}s ease-out ${delay}s forwards;
|
|
523
|
+
--drift:${drift}px;
|
|
524
|
+
`;
|
|
525
|
+
container.appendChild(piece);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Inject keyframes once
|
|
529
|
+
if (!document.getElementById("confetti-style")) {
|
|
530
|
+
const style = document.createElement("style");
|
|
531
|
+
style.id = "confetti-style";
|
|
532
|
+
style.textContent = `
|
|
533
|
+
@keyframes confettiFall {
|
|
534
|
+
0% { transform: translateY(0) translateX(0) rotate(0deg); opacity: 1; }
|
|
535
|
+
100% { transform: translateY(100vh) translateX(var(--drift)) rotate(720deg); opacity: 0; }
|
|
536
|
+
}
|
|
537
|
+
`;
|
|
538
|
+
document.head.appendChild(style);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
setTimeout(() => container.remove(), 4000);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Toggle panel collapse
|
|
545
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
546
|
+
const toggle = document.getElementById("upload-panel-toggle");
|
|
547
|
+
if (toggle) {
|
|
548
|
+
toggle.addEventListener("click", () => {
|
|
549
|
+
const log = document.getElementById("upload-log");
|
|
550
|
+
if (log) log.classList.toggle("collapsed");
|
|
551
|
+
toggle.classList.toggle("flipped");
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
});
|