@officexapp/catalogs-cli 0.2.8 → 0.3.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/dist/index.js +1247 -61
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { readFileSync as readFileSync5 } from "fs";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
-
import { dirname as dirname3, join as
|
|
7
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
10
|
var DEFAULT_API_URL = "https://api.catalogkit.cc";
|
|
@@ -233,7 +233,7 @@ Check status later: catalogs video status ${videoId}
|
|
|
233
233
|
`);
|
|
234
234
|
}
|
|
235
235
|
function sleep(ms) {
|
|
236
|
-
return new Promise((
|
|
236
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
// src/commands/video-status.ts
|
|
@@ -265,9 +265,7 @@ async function videoStatus(videoId) {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
// src/commands/catalog-push.ts
|
|
268
|
-
import {
|
|
269
|
-
import { resolve as resolve2, extname as extname2, dirname } from "path";
|
|
270
|
-
import { pathToFileURL } from "url";
|
|
268
|
+
import { resolve as resolve3, dirname } from "path";
|
|
271
269
|
import ora3 from "ora";
|
|
272
270
|
|
|
273
271
|
// src/lib/serialize.ts
|
|
@@ -315,6 +313,97 @@ function validateCatalog(schema) {
|
|
|
315
313
|
}
|
|
316
314
|
return errors;
|
|
317
315
|
}
|
|
316
|
+
function deepValidateCatalog(schema) {
|
|
317
|
+
const errors = [];
|
|
318
|
+
const warnings = [];
|
|
319
|
+
const pages = schema.pages || {};
|
|
320
|
+
const pageIds = new Set(Object.keys(pages));
|
|
321
|
+
const routing = schema.routing || {};
|
|
322
|
+
const edges = routing.edges || [];
|
|
323
|
+
const entry = routing.entry;
|
|
324
|
+
const KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
325
|
+
"heading",
|
|
326
|
+
"paragraph",
|
|
327
|
+
"image",
|
|
328
|
+
"video",
|
|
329
|
+
"html",
|
|
330
|
+
"banner",
|
|
331
|
+
"callout",
|
|
332
|
+
"divider",
|
|
333
|
+
"pricing_card",
|
|
334
|
+
"testimonial",
|
|
335
|
+
"faq",
|
|
336
|
+
"timeline",
|
|
337
|
+
"file_download",
|
|
338
|
+
"iframe",
|
|
339
|
+
"short_text",
|
|
340
|
+
"email",
|
|
341
|
+
"phone",
|
|
342
|
+
"url",
|
|
343
|
+
"number",
|
|
344
|
+
"password",
|
|
345
|
+
"long_text",
|
|
346
|
+
"multiple_choice",
|
|
347
|
+
"checkboxes",
|
|
348
|
+
"dropdown",
|
|
349
|
+
"slider",
|
|
350
|
+
"star_rating",
|
|
351
|
+
"switch",
|
|
352
|
+
"checkbox",
|
|
353
|
+
"payment"
|
|
354
|
+
]);
|
|
355
|
+
if (entry && !pageIds.has(entry)) {
|
|
356
|
+
errors.push(`routing.entry "${entry}" does not exist in pages`);
|
|
357
|
+
}
|
|
358
|
+
for (const edge of edges) {
|
|
359
|
+
if (!pageIds.has(edge.from)) {
|
|
360
|
+
errors.push(`routing edge from "${edge.from}" references non-existent page`);
|
|
361
|
+
}
|
|
362
|
+
if (edge.to != null && !pageIds.has(edge.to)) {
|
|
363
|
+
errors.push(`routing edge to "${edge.to}" references non-existent page`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
for (const [pageId, page] of Object.entries(pages)) {
|
|
367
|
+
const components = page.components || [];
|
|
368
|
+
const compIds = /* @__PURE__ */ new Set();
|
|
369
|
+
for (const comp of components) {
|
|
370
|
+
if (comp.id && compIds.has(comp.id)) {
|
|
371
|
+
errors.push(`page "${pageId}": duplicate component ID "${comp.id}"`);
|
|
372
|
+
}
|
|
373
|
+
if (comp.id) compIds.add(comp.id);
|
|
374
|
+
if (comp.type && !KNOWN_TYPES.has(comp.type)) {
|
|
375
|
+
warnings.push(`page "${pageId}": unknown component type "${comp.type}"`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (page.offer?.accept_field) {
|
|
379
|
+
if (!compIds.has(page.offer.accept_field)) {
|
|
380
|
+
errors.push(
|
|
381
|
+
`page "${pageId}": offer.accept_field "${page.offer.accept_field}" does not match any component ID on this page`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (entry && pageIds.has(entry)) {
|
|
387
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
388
|
+
const queue = [entry];
|
|
389
|
+
reachable.add(entry);
|
|
390
|
+
while (queue.length > 0) {
|
|
391
|
+
const id = queue.shift();
|
|
392
|
+
for (const edge of edges) {
|
|
393
|
+
if (edge.from === id && !reachable.has(edge.to)) {
|
|
394
|
+
reachable.add(edge.to);
|
|
395
|
+
queue.push(edge.to);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const id of pageIds) {
|
|
400
|
+
if (!reachable.has(id)) {
|
|
401
|
+
warnings.push(`page "${id}" is unreachable from entry "${entry}"`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return { errors, warnings };
|
|
406
|
+
}
|
|
318
407
|
|
|
319
408
|
// src/lib/resolve-assets.ts
|
|
320
409
|
import { existsSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
@@ -557,29 +646,33 @@ async function uploadFile(localPath, filename, api) {
|
|
|
557
646
|
return downloadUrl;
|
|
558
647
|
}
|
|
559
648
|
|
|
560
|
-
// src/
|
|
561
|
-
|
|
649
|
+
// src/lib/load-file.ts
|
|
650
|
+
import { resolve as resolve2, extname as extname2 } from "path";
|
|
651
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
652
|
+
async function loadCatalogFile(file) {
|
|
562
653
|
const abs = resolve2(file);
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
654
|
+
const ext = extname2(file).toLowerCase();
|
|
655
|
+
const isTs = ext === ".ts" || ext === ".mts";
|
|
656
|
+
if (isTs) {
|
|
657
|
+
const { tsImport } = await import("tsx/esm/api");
|
|
658
|
+
const mod = await tsImport(abs, import.meta.url);
|
|
659
|
+
let raw = mod.default ?? mod;
|
|
660
|
+
if (raw.__esModule && raw.default) raw = raw.default;
|
|
661
|
+
return serializeCatalog(raw);
|
|
662
|
+
} else {
|
|
663
|
+
const raw = readFileSync3(abs, "utf-8");
|
|
664
|
+
return JSON.parse(raw);
|
|
665
|
+
}
|
|
567
666
|
}
|
|
667
|
+
|
|
668
|
+
// src/commands/catalog-push.ts
|
|
568
669
|
async function catalogPush(file, opts) {
|
|
569
670
|
const config = requireConfig();
|
|
570
671
|
const api = new ApiClient(config);
|
|
571
672
|
await printIdentity(api);
|
|
572
|
-
const ext = extname2(file).toLowerCase();
|
|
573
|
-
const isTs = ext === ".ts" || ext === ".mts";
|
|
574
673
|
let schema;
|
|
575
674
|
try {
|
|
576
|
-
|
|
577
|
-
const rawCatalog = await loadTsFile(file);
|
|
578
|
-
schema = serializeCatalog(rawCatalog);
|
|
579
|
-
} else {
|
|
580
|
-
const raw = readFileSync3(file, "utf-8");
|
|
581
|
-
schema = JSON.parse(raw);
|
|
582
|
-
}
|
|
675
|
+
schema = await loadCatalogFile(file);
|
|
583
676
|
} catch (err) {
|
|
584
677
|
console.error(`Failed to read ${file}: ${err.message}`);
|
|
585
678
|
process.exit(1);
|
|
@@ -591,7 +684,7 @@ async function catalogPush(file, opts) {
|
|
|
591
684
|
}
|
|
592
685
|
process.exit(1);
|
|
593
686
|
}
|
|
594
|
-
const catalogDir = dirname(
|
|
687
|
+
const catalogDir = dirname(resolve3(file));
|
|
595
688
|
const assetSpinner = ora3("Checking for local assets...").start();
|
|
596
689
|
try {
|
|
597
690
|
const result = await uploadLocalAssets(schema, catalogDir, api, (msg) => {
|
|
@@ -680,27 +773,11 @@ async function catalogList() {
|
|
|
680
773
|
}
|
|
681
774
|
|
|
682
775
|
// src/commands/catalog-dev.ts
|
|
683
|
-
import { resolve as
|
|
776
|
+
import { resolve as resolve4, dirname as dirname2, extname as extname3, join as join2 } from "path";
|
|
684
777
|
import { existsSync as existsSync2, readFileSync as readFileSync4, statSync as statSync3, watch } from "fs";
|
|
685
|
-
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
686
778
|
import { createServer } from "http";
|
|
687
779
|
import ora5 from "ora";
|
|
688
780
|
var DEFAULT_PORT = 3456;
|
|
689
|
-
async function loadCatalogFile(file) {
|
|
690
|
-
const abs = resolve3(file);
|
|
691
|
-
const ext = extname3(file).toLowerCase();
|
|
692
|
-
const isTs = ext === ".ts" || ext === ".mts";
|
|
693
|
-
if (isTs) {
|
|
694
|
-
const { register } = await import("module");
|
|
695
|
-
register("tsx/esm", pathToFileURL2("./"));
|
|
696
|
-
const url = pathToFileURL2(abs).href + `?t=${Date.now()}`;
|
|
697
|
-
const mod = await import(url);
|
|
698
|
-
return serializeCatalog(mod.default ?? mod);
|
|
699
|
-
} else {
|
|
700
|
-
const raw = readFileSync4(abs, "utf-8");
|
|
701
|
-
return JSON.parse(raw);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
781
|
var MIME_TYPES = {
|
|
705
782
|
".html": "text/html",
|
|
706
783
|
".js": "application/javascript",
|
|
@@ -730,7 +807,7 @@ function getMime(filepath) {
|
|
|
730
807
|
const ext = extname3(filepath).toLowerCase();
|
|
731
808
|
return MIME_TYPES[ext] || "application/octet-stream";
|
|
732
809
|
}
|
|
733
|
-
function buildPreviewHtml(schema, port) {
|
|
810
|
+
function buildPreviewHtml(schema, port, validation, devConfig) {
|
|
734
811
|
const schemaJson = JSON.stringify(schema).replace(/<\/script/gi, "<\\/script");
|
|
735
812
|
const themeColor = schema.settings?.theme?.primary_color || "#6366f1";
|
|
736
813
|
return `<!DOCTYPE html>
|
|
@@ -927,6 +1004,55 @@ function buildPreviewHtml(schema, port) {
|
|
|
927
1004
|
padding: 32px; text-align: center;
|
|
928
1005
|
box-shadow: 0 2px 12px rgba(0,0,0,0.04);
|
|
929
1006
|
}
|
|
1007
|
+
|
|
1008
|
+
/* Validation banner */
|
|
1009
|
+
.validation-banner {
|
|
1010
|
+
position: fixed; bottom: 0; left: 0; right: 0; z-index: 99999;
|
|
1011
|
+
background: #7f1d1d; color: #fecaca; font-family: monospace; font-size: 12px;
|
|
1012
|
+
padding: 10px 16px; max-height: 200px; overflow-y: auto;
|
|
1013
|
+
border-top: 2px solid #ef4444;
|
|
1014
|
+
}
|
|
1015
|
+
.validation-banner .vb-header {
|
|
1016
|
+
display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;
|
|
1017
|
+
}
|
|
1018
|
+
.validation-banner .vb-header strong { color: #fca5a5; }
|
|
1019
|
+
.validation-banner .vb-dismiss {
|
|
1020
|
+
background: rgba(255,255,255,0.1); border: none; color: #fca5a5; border-radius: 4px;
|
|
1021
|
+
padding: 2px 8px; cursor: pointer; font-size: 11px;
|
|
1022
|
+
}
|
|
1023
|
+
.validation-banner .vb-dismiss:hover { background: rgba(255,255,255,0.2); }
|
|
1024
|
+
.validation-banner .vb-error { color: #fca5a5; }
|
|
1025
|
+
.validation-banner .vb-warn { color: #fde68a; }
|
|
1026
|
+
|
|
1027
|
+
/* Debug panel */
|
|
1028
|
+
.debug-panel {
|
|
1029
|
+
position: fixed; bottom: 0; right: 0; z-index: 99998;
|
|
1030
|
+
width: 380px; max-height: 60vh; background: #1e1b4b; color: #e0e7ff;
|
|
1031
|
+
font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px;
|
|
1032
|
+
border-top-left-radius: 12px; overflow: hidden;
|
|
1033
|
+
box-shadow: -4px -4px 24px rgba(0,0,0,0.3); display: none;
|
|
1034
|
+
}
|
|
1035
|
+
.debug-panel.open { display: flex; flex-direction: column; }
|
|
1036
|
+
.debug-panel .dp-header {
|
|
1037
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
1038
|
+
padding: 8px 12px; background: #312e81; border-bottom: 1px solid #4338ca;
|
|
1039
|
+
font-weight: 600; font-size: 12px;
|
|
1040
|
+
}
|
|
1041
|
+
.debug-panel .dp-close {
|
|
1042
|
+
background: none; border: none; color: #a5b4fc; cursor: pointer; font-size: 14px;
|
|
1043
|
+
}
|
|
1044
|
+
.debug-panel .dp-body {
|
|
1045
|
+
padding: 10px 12px; overflow-y: auto; flex: 1;
|
|
1046
|
+
}
|
|
1047
|
+
.debug-panel .dp-section { margin-bottom: 10px; }
|
|
1048
|
+
.debug-panel .dp-label { color: #818cf8; font-weight: 600; font-size: 10px; text-transform: uppercase; margin-bottom: 4px; }
|
|
1049
|
+
.debug-panel .dp-badge {
|
|
1050
|
+
display: inline-block; background: #4338ca; color: #c7d2fe; padding: 1px 8px;
|
|
1051
|
+
border-radius: 4px; font-size: 11px; font-weight: 600;
|
|
1052
|
+
}
|
|
1053
|
+
.debug-panel pre { margin: 0; white-space: pre-wrap; word-break: break-all; color: #c7d2fe; line-height: 1.5; }
|
|
1054
|
+
.dev-banner .stub-tag.debug-btn { cursor: pointer; transition: all 0.15s ease; }
|
|
1055
|
+
.dev-banner .stub-tag.debug-btn:hover { background: rgba(255,255,255,0.2); color: #a5b4fc; }
|
|
930
1056
|
</style>
|
|
931
1057
|
</head>
|
|
932
1058
|
<body>
|
|
@@ -935,9 +1061,10 @@ function buildPreviewHtml(schema, port) {
|
|
|
935
1061
|
<span class="label">LOCAL DEV</span>
|
|
936
1062
|
<span class="slug">${schema.slug || "catalog"}</span>
|
|
937
1063
|
<span class="stub-tags">
|
|
938
|
-
<span class="stub-tag"
|
|
939
|
-
<span class="stub-tag">
|
|
1064
|
+
<span class="stub-tag" id="checkout-tag">${devConfig?.stripeEnabled ? "Checkout: live (test)" : "Checkout: stubbed"}</span>
|
|
1065
|
+
<span class="stub-tag">Events: local</span>
|
|
940
1066
|
<span class="stub-tag clickable" id="pages-btn">Pages</span>
|
|
1067
|
+
<span class="stub-tag debug-btn" id="debug-btn">Debug</span>
|
|
941
1068
|
</span>
|
|
942
1069
|
</div>
|
|
943
1070
|
<div class="pages-overlay" id="pages-overlay">
|
|
@@ -948,8 +1075,15 @@ function buildPreviewHtml(schema, port) {
|
|
|
948
1075
|
<div class="inspector-tooltip" id="inspector-tooltip" style="display:none"></div>
|
|
949
1076
|
<div class="inspector-active-banner" id="inspector-banner" style="display:none">Inspector active — hover elements, click to copy</div>
|
|
950
1077
|
<div id="catalog-root"></div>
|
|
1078
|
+
<div class="debug-panel" id="debug-panel">
|
|
1079
|
+
<div class="dp-header"><span>Debug Panel</span><button class="dp-close" id="debug-close">×</button></div>
|
|
1080
|
+
<div class="dp-body" id="debug-body"></div>
|
|
1081
|
+
</div>
|
|
1082
|
+
<div class="validation-banner" id="validation-banner" style="display:none"></div>
|
|
951
1083
|
|
|
952
1084
|
<script id="__catalog_data" type="application/json">${schemaJson}</script>
|
|
1085
|
+
<script id="__validation_data" type="application/json">${JSON.stringify(validation || { errors: [], warnings: [] })}</script>
|
|
1086
|
+
<script id="__dev_config" type="application/json">${JSON.stringify(devConfig || { stripeEnabled: false, port })}</script>
|
|
953
1087
|
|
|
954
1088
|
<script type="module">
|
|
955
1089
|
import React from 'https://esm.sh/react@18';
|
|
@@ -959,6 +1093,61 @@ function buildPreviewHtml(schema, port) {
|
|
|
959
1093
|
const schema = JSON.parse(document.getElementById('__catalog_data').textContent);
|
|
960
1094
|
const themeColor = '${themeColor}';
|
|
961
1095
|
|
|
1096
|
+
// --- Visibility condition engine (ported from shared/engine/conditions.ts) ---
|
|
1097
|
+
const devContext = (() => {
|
|
1098
|
+
const params = {};
|
|
1099
|
+
new URLSearchParams(window.location.search).forEach((v, k) => { params[k] = v; });
|
|
1100
|
+
return { url_params: params, hints: {}, quiz_scores: null, video_state: null };
|
|
1101
|
+
})();
|
|
1102
|
+
|
|
1103
|
+
function resolveConditionValue(rule, formState, ctx) {
|
|
1104
|
+
const source = rule.source || 'field';
|
|
1105
|
+
if (source === 'field') return rule.field != null ? formState[rule.field] : undefined;
|
|
1106
|
+
if (source === 'url_param') return rule.param != null ? ctx.url_params[rule.param] : undefined;
|
|
1107
|
+
if (source === 'hint') return rule.param != null ? ctx.hints[rule.param] : undefined;
|
|
1108
|
+
if (source === 'score') return 0;
|
|
1109
|
+
if (source === 'video') return undefined;
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function applyConditionOperator(op, actual, expected) {
|
|
1114
|
+
switch (op) {
|
|
1115
|
+
case 'equals': return String(actual ?? '') === String(expected ?? '');
|
|
1116
|
+
case 'not_equals': return String(actual ?? '') !== String(expected ?? '');
|
|
1117
|
+
case 'contains':
|
|
1118
|
+
if (typeof actual === 'string') return actual.includes(String(expected));
|
|
1119
|
+
if (Array.isArray(actual)) return actual.includes(expected);
|
|
1120
|
+
return false;
|
|
1121
|
+
case 'not_contains':
|
|
1122
|
+
if (typeof actual === 'string') return !actual.includes(String(expected));
|
|
1123
|
+
if (Array.isArray(actual)) return !actual.includes(expected);
|
|
1124
|
+
return true;
|
|
1125
|
+
case 'greater_than': return Number(actual) > Number(expected);
|
|
1126
|
+
case 'greater_than_or_equal': return Number(actual) >= Number(expected);
|
|
1127
|
+
case 'less_than': return Number(actual) < Number(expected);
|
|
1128
|
+
case 'less_than_or_equal': return Number(actual) <= Number(expected);
|
|
1129
|
+
case 'is_empty': return actual == null || actual === '' || (Array.isArray(actual) && actual.length === 0);
|
|
1130
|
+
case 'is_not_empty': return !(actual == null || actual === '' || (Array.isArray(actual) && actual.length === 0));
|
|
1131
|
+
case 'matches_regex':
|
|
1132
|
+
try { const p = String(expected); if (p.length > 200) return false; return new RegExp(p).test(String(actual ?? '')); } catch { return false; }
|
|
1133
|
+
case 'in':
|
|
1134
|
+
if (Array.isArray(expected)) return expected.includes(actual);
|
|
1135
|
+
if (typeof expected === 'string') return expected.split(',').map(s => s.trim()).includes(String(actual));
|
|
1136
|
+
return false;
|
|
1137
|
+
default: return false;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function evaluateConditionGroup(group, formState, ctx) {
|
|
1142
|
+
if (!group || !group.rules) return true;
|
|
1143
|
+
const evaluate = (item) => {
|
|
1144
|
+
if (item.match && item.rules) return evaluateConditionGroup(item, formState, ctx);
|
|
1145
|
+
const actual = resolveConditionValue(item, formState, ctx);
|
|
1146
|
+
return applyConditionOperator(item.operator, actual, item.value);
|
|
1147
|
+
};
|
|
1148
|
+
return group.match === 'all' ? group.rules.every(evaluate) : group.rules.some(evaluate);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
962
1151
|
// --- Markdown-ish text rendering ---
|
|
963
1152
|
function inlineMarkdown(text) {
|
|
964
1153
|
return text
|
|
@@ -1260,13 +1449,7 @@ function buildPreviewHtml(schema, port) {
|
|
|
1260
1449
|
return h(SwitchInput, { comp, type, formState, onFieldChange, isCover, compClass, compStyle });
|
|
1261
1450
|
|
|
1262
1451
|
case 'payment':
|
|
1263
|
-
return h(
|
|
1264
|
-
h('h3', null, 'Stripe Checkout (Dev Stub)'),
|
|
1265
|
-
h('p', null, 'Payment processing is disabled in local dev mode.'),
|
|
1266
|
-
props.amount ? h('p', { className: 'mt-2 font-bold text-lg' },
|
|
1267
|
-
(props.currency || 'USD').toUpperCase() + ' ' + (props.amount / 100).toFixed(2)
|
|
1268
|
-
) : null
|
|
1269
|
-
);
|
|
1452
|
+
return h(PaymentComponent, { comp, formState, isCover, compClass, compStyle });
|
|
1270
1453
|
|
|
1271
1454
|
default:
|
|
1272
1455
|
return h('div', {
|
|
@@ -1427,6 +1610,267 @@ function buildPreviewHtml(schema, port) {
|
|
|
1427
1610
|
);
|
|
1428
1611
|
}
|
|
1429
1612
|
|
|
1613
|
+
// --- Dev Config ---
|
|
1614
|
+
const devConfig = JSON.parse(document.getElementById('__dev_config').textContent);
|
|
1615
|
+
|
|
1616
|
+
// --- Local Event Emitter ---
|
|
1617
|
+
const devEvents = {
|
|
1618
|
+
_sse: null,
|
|
1619
|
+
init() {
|
|
1620
|
+
this._sse = new EventSource('/__dev_events_stream');
|
|
1621
|
+
this._sse.onerror = () => {};
|
|
1622
|
+
},
|
|
1623
|
+
emit(type, data) {
|
|
1624
|
+
const event = { type, timestamp: new Date().toISOString(), data };
|
|
1625
|
+
// Fire to local SSE listeners (agents can listen via /__dev_events_stream)
|
|
1626
|
+
fetch('/__dev_event', {
|
|
1627
|
+
method: 'POST',
|
|
1628
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1629
|
+
body: JSON.stringify(event),
|
|
1630
|
+
}).catch(() => {});
|
|
1631
|
+
// Also dispatch as browser event for debug panel
|
|
1632
|
+
window.dispatchEvent(new CustomEvent('devEvent', { detail: event }));
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
devEvents.init();
|
|
1636
|
+
|
|
1637
|
+
// --- Payment Component ---
|
|
1638
|
+
function PaymentComponent({ comp, formState, isCover, compClass, compStyle }) {
|
|
1639
|
+
const props = comp.props || {};
|
|
1640
|
+
const [loading, setLoading] = React.useState(false);
|
|
1641
|
+
const [error, setError] = React.useState(null);
|
|
1642
|
+
|
|
1643
|
+
const handleCheckout = React.useCallback(async () => {
|
|
1644
|
+
devEvents.emit('checkout_started', { component_id: comp.id, amount: props.amount, currency: props.currency });
|
|
1645
|
+
setLoading(true);
|
|
1646
|
+
setError(null);
|
|
1647
|
+
try {
|
|
1648
|
+
const res = await fetch('/__dev_checkout', {
|
|
1649
|
+
method: 'POST',
|
|
1650
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1651
|
+
body: JSON.stringify({
|
|
1652
|
+
line_items: [{
|
|
1653
|
+
title: props.title || comp.id,
|
|
1654
|
+
amount_cents: props.amount,
|
|
1655
|
+
currency: (props.currency || 'usd').toLowerCase(),
|
|
1656
|
+
quantity: 1,
|
|
1657
|
+
stripe_price_id: props.stripe_price_id,
|
|
1658
|
+
payment_type: props.checkout_type === 'redirect' ? 'one_time' : (schema.settings?.checkout?.payment_type || 'one_time'),
|
|
1659
|
+
}],
|
|
1660
|
+
form_state: formState,
|
|
1661
|
+
catalog_slug: schema.slug,
|
|
1662
|
+
}),
|
|
1663
|
+
});
|
|
1664
|
+
const data = await res.json();
|
|
1665
|
+
if (data.session_url) {
|
|
1666
|
+
devEvents.emit('checkout_redirect', { session_id: data.session_id });
|
|
1667
|
+
window.location.href = data.session_url;
|
|
1668
|
+
} else if (data.error) {
|
|
1669
|
+
setError(data.error);
|
|
1670
|
+
}
|
|
1671
|
+
} catch (err) {
|
|
1672
|
+
setError(err.message || 'Checkout failed');
|
|
1673
|
+
} finally {
|
|
1674
|
+
setLoading(false);
|
|
1675
|
+
}
|
|
1676
|
+
}, [comp.id, props, formState]);
|
|
1677
|
+
|
|
1678
|
+
// No Stripe key \u2014 show informative stub
|
|
1679
|
+
if (!devConfig.stripeEnabled) {
|
|
1680
|
+
return h('div', { className: 'checkout-stub ' + compClass, style: compStyle },
|
|
1681
|
+
h('h3', null, 'Stripe Checkout'),
|
|
1682
|
+
h('p', null, 'Add STRIPE_SECRET_KEY to your .env to enable real checkout in dev.'),
|
|
1683
|
+
props.amount ? h('p', { className: 'mt-2 font-bold text-lg' },
|
|
1684
|
+
(props.currency || 'USD').toUpperCase() + ' ' + (props.amount / 100).toFixed(2)
|
|
1685
|
+
) : null,
|
|
1686
|
+
h('details', { className: 'mt-3 text-left', style: { fontSize: '11px', color: '#92400e' } },
|
|
1687
|
+
h('summary', { style: { cursor: 'pointer' } }, 'Checkout payload'),
|
|
1688
|
+
h('pre', { style: { whiteSpace: 'pre-wrap', marginTop: '4px' } },
|
|
1689
|
+
JSON.stringify({ amount: props.amount, currency: props.currency, stripe_price_id: props.stripe_price_id, payment_type: schema.settings?.checkout?.payment_type }, null, 2)
|
|
1690
|
+
)
|
|
1691
|
+
)
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// Real Stripe checkout
|
|
1696
|
+
return h('div', { className: compClass, style: compStyle },
|
|
1697
|
+
h('button', {
|
|
1698
|
+
className: 'cf-btn-primary w-full text-white',
|
|
1699
|
+
style: { backgroundColor: themeColor, opacity: loading ? 0.7 : 1 },
|
|
1700
|
+
onClick: handleCheckout,
|
|
1701
|
+
disabled: loading,
|
|
1702
|
+
},
|
|
1703
|
+
loading ? 'Redirecting to Stripe...' : (props.button_text || (props.amount ? ((props.currency || 'USD').toUpperCase() + ' ' + (props.amount / 100).toFixed(2) + ' \u2014 Pay Now') : 'Checkout'))
|
|
1704
|
+
),
|
|
1705
|
+
error ? h('p', { className: 'text-red-500 text-sm mt-2 text-center' }, error) : null
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// --- Cart Components ---
|
|
1710
|
+
const cartIconPath = 'M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z';
|
|
1711
|
+
|
|
1712
|
+
function CartButton({ itemCount, onClick }) {
|
|
1713
|
+
if (itemCount === 0) return null;
|
|
1714
|
+
return h('button', {
|
|
1715
|
+
onClick,
|
|
1716
|
+
className: 'fixed bottom-6 right-6 z-[90] flex items-center gap-2 rounded-full px-5 py-3.5 text-white font-semibold shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl active:scale-95',
|
|
1717
|
+
style: { backgroundColor: themeColor, fontFamily: 'var(--font-display)' },
|
|
1718
|
+
},
|
|
1719
|
+
h('svg', { className: 'w-5 h-5', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
|
|
1720
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: cartIconPath })
|
|
1721
|
+
),
|
|
1722
|
+
h('span', null, itemCount),
|
|
1723
|
+
h('span', { className: 'text-sm opacity-80' }, itemCount === 1 ? 'item' : 'items')
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function CartDrawer({ items, isOpen, onToggle, onRemove }) {
|
|
1728
|
+
React.useEffect(() => {
|
|
1729
|
+
if (!isOpen) return;
|
|
1730
|
+
const handleKey = (e) => { if (e.key === 'Escape') onToggle(); };
|
|
1731
|
+
window.addEventListener('keydown', handleKey);
|
|
1732
|
+
return () => window.removeEventListener('keydown', handleKey);
|
|
1733
|
+
}, [isOpen, onToggle]);
|
|
1734
|
+
|
|
1735
|
+
return h(React.Fragment, null,
|
|
1736
|
+
// Backdrop
|
|
1737
|
+
h('div', {
|
|
1738
|
+
className: 'fixed inset-0 z-[95] bg-black/30 backdrop-blur-sm transition-opacity duration-300 ' + (isOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'),
|
|
1739
|
+
onClick: onToggle,
|
|
1740
|
+
}),
|
|
1741
|
+
// Drawer
|
|
1742
|
+
h('div', {
|
|
1743
|
+
className: 'fixed top-0 right-0 bottom-0 z-[96] w-full max-w-md bg-white shadow-2xl transition-transform duration-300 ease-out flex flex-col ' + (isOpen ? 'translate-x-0' : 'translate-x-full'),
|
|
1744
|
+
style: { fontFamily: 'var(--font-display)' },
|
|
1745
|
+
},
|
|
1746
|
+
// Header
|
|
1747
|
+
h('div', { className: 'flex items-center justify-between px-6 py-5 border-b border-gray-100' },
|
|
1748
|
+
h('div', { className: 'flex items-center gap-3' },
|
|
1749
|
+
h('div', { className: 'w-9 h-9 rounded-xl flex items-center justify-center', style: { backgroundColor: themeColor + '12' } },
|
|
1750
|
+
h('svg', { className: 'w-5 h-5', style: { color: themeColor }, fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
|
|
1751
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: cartIconPath })
|
|
1752
|
+
)
|
|
1753
|
+
),
|
|
1754
|
+
h('div', null,
|
|
1755
|
+
h('h2', { className: 'text-lg font-bold text-gray-900' }, 'Your Cart'),
|
|
1756
|
+
h('p', { className: 'text-xs text-gray-400' }, items.length + ' ' + (items.length === 1 ? 'item' : 'items') + ' selected')
|
|
1757
|
+
)
|
|
1758
|
+
),
|
|
1759
|
+
h('button', { onClick: onToggle, className: 'w-9 h-9 flex items-center justify-center rounded-xl text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-all' },
|
|
1760
|
+
h('svg', { className: 'w-5 h-5', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
|
|
1761
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M6 18L18 6M6 6l12 12' })
|
|
1762
|
+
)
|
|
1763
|
+
)
|
|
1764
|
+
),
|
|
1765
|
+
// Items
|
|
1766
|
+
h('div', { className: 'flex-1 overflow-y-auto px-6 py-4' },
|
|
1767
|
+
items.length === 0
|
|
1768
|
+
? h('div', { className: 'flex flex-col items-center justify-center h-full text-center py-16' },
|
|
1769
|
+
h('div', { className: 'w-16 h-16 rounded-2xl flex items-center justify-center mb-4', style: { backgroundColor: themeColor + '08' } },
|
|
1770
|
+
h('svg', { className: 'w-8 h-8 text-gray-300', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 1.5 },
|
|
1771
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: cartIconPath })
|
|
1772
|
+
)
|
|
1773
|
+
),
|
|
1774
|
+
h('p', { className: 'text-gray-400 text-sm font-medium' }, 'No offers accepted yet'),
|
|
1775
|
+
h('p', { className: 'text-gray-300 text-xs mt-1' }, 'Accept offers as you browse to add them here')
|
|
1776
|
+
)
|
|
1777
|
+
: h('div', { className: 'space-y-3' },
|
|
1778
|
+
...items.map(item => h('div', { key: item.offer_id, className: 'group flex items-start gap-4 p-4 rounded-2xl border border-gray-100 bg-gray-50/50 hover:bg-white hover:border-gray-200 hover:shadow-sm transition-all duration-200' },
|
|
1779
|
+
item.image ? h('img', { src: item.image, alt: item.title, className: 'w-14 h-14 rounded-xl object-cover flex-shrink-0' }) : null,
|
|
1780
|
+
h('div', { className: 'flex-1 min-w-0' },
|
|
1781
|
+
h('h3', { className: 'text-sm font-semibold text-gray-900 truncate' }, item.title),
|
|
1782
|
+
item.price_display ? h('p', { className: 'text-sm font-bold mt-0.5', style: { color: themeColor } }, item.price_display) : null,
|
|
1783
|
+
item.price_subtext ? h('p', { className: 'text-xs text-gray-400 mt-0.5' }, item.price_subtext) : null
|
|
1784
|
+
),
|
|
1785
|
+
h('button', { onClick: () => onRemove(item.offer_id), className: 'w-7 h-7 flex-shrink-0 flex items-center justify-center rounded-lg text-gray-300 hover:text-red-500 hover:bg-red-50 transition-all opacity-0 group-hover:opacity-100' },
|
|
1786
|
+
h('svg', { className: 'w-4 h-4', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
|
|
1787
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0' })
|
|
1788
|
+
)
|
|
1789
|
+
)
|
|
1790
|
+
))
|
|
1791
|
+
)
|
|
1792
|
+
),
|
|
1793
|
+
// Footer
|
|
1794
|
+
items.length > 0 ? h('div', { className: 'border-t border-gray-100 px-6 py-5 space-y-4' },
|
|
1795
|
+
h('div', { className: 'flex items-center justify-between' },
|
|
1796
|
+
h('span', { className: 'text-sm font-medium text-gray-500' }, items.length + ' ' + (items.length === 1 ? 'offer' : 'offers') + ' selected'),
|
|
1797
|
+
h('div', { className: 'flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold', style: { backgroundColor: themeColor + '10', color: themeColor } },
|
|
1798
|
+
h('svg', { className: 'w-3.5 h-3.5', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
|
|
1799
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z' })
|
|
1800
|
+
),
|
|
1801
|
+
'Added to order'
|
|
1802
|
+
)
|
|
1803
|
+
),
|
|
1804
|
+
h(CartCheckoutButton, { items, themeColor })
|
|
1805
|
+
) : null
|
|
1806
|
+
)
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function CartCheckoutButton({ items, themeColor }) {
|
|
1811
|
+
const [loading, setLoading] = React.useState(false);
|
|
1812
|
+
const [error, setError] = React.useState(null);
|
|
1813
|
+
|
|
1814
|
+
const handleCheckout = React.useCallback(async () => {
|
|
1815
|
+
const state = window.__devDebugState;
|
|
1816
|
+
devEvents.emit('cart_checkout_started', { items: items.map(i => ({ offer_id: i.offer_id, title: i.title })) });
|
|
1817
|
+
setLoading(true);
|
|
1818
|
+
setError(null);
|
|
1819
|
+
try {
|
|
1820
|
+
const res = await fetch('/__dev_checkout', {
|
|
1821
|
+
method: 'POST',
|
|
1822
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1823
|
+
body: JSON.stringify({
|
|
1824
|
+
line_items: items.map(item => ({
|
|
1825
|
+
title: item.title,
|
|
1826
|
+
amount_cents: item.amount_cents,
|
|
1827
|
+
currency: item.currency || 'usd',
|
|
1828
|
+
quantity: 1,
|
|
1829
|
+
stripe_price_id: item.stripe_price_id,
|
|
1830
|
+
payment_type: schema.settings?.checkout?.payment_type || 'one_time',
|
|
1831
|
+
})),
|
|
1832
|
+
form_state: state?.formState || {},
|
|
1833
|
+
catalog_slug: schema.slug,
|
|
1834
|
+
}),
|
|
1835
|
+
});
|
|
1836
|
+
const data = await res.json();
|
|
1837
|
+
if (data.session_url) {
|
|
1838
|
+
devEvents.emit('checkout_redirect', { session_id: data.session_id });
|
|
1839
|
+
window.location.href = data.session_url;
|
|
1840
|
+
} else if (data.error) {
|
|
1841
|
+
setError(data.error);
|
|
1842
|
+
}
|
|
1843
|
+
} catch (err) {
|
|
1844
|
+
setError(err.message || 'Checkout failed');
|
|
1845
|
+
} finally {
|
|
1846
|
+
setLoading(false);
|
|
1847
|
+
}
|
|
1848
|
+
}, [items]);
|
|
1849
|
+
|
|
1850
|
+
if (!devConfig.stripeEnabled) {
|
|
1851
|
+
return h('div', { className: 'checkout-stub' },
|
|
1852
|
+
h('h3', null, 'Checkout'),
|
|
1853
|
+
h('p', null, 'Add STRIPE_SECRET_KEY to .env for real checkout.'),
|
|
1854
|
+
h('details', { className: 'mt-2 text-left', style: { fontSize: '11px', color: '#92400e' } },
|
|
1855
|
+
h('summary', { style: { cursor: 'pointer' } }, 'Cart payload'),
|
|
1856
|
+
h('pre', { style: { whiteSpace: 'pre-wrap', marginTop: '4px' } },
|
|
1857
|
+
JSON.stringify(items.map(i => ({ offer_id: i.offer_id, title: i.title, price: i.price_display })), null, 2)
|
|
1858
|
+
)
|
|
1859
|
+
)
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
return h('div', { className: 'space-y-2' },
|
|
1864
|
+
h('button', {
|
|
1865
|
+
className: 'cf-btn-primary w-full text-white',
|
|
1866
|
+
style: { backgroundColor: themeColor, opacity: loading ? 0.7 : 1 },
|
|
1867
|
+
onClick: handleCheckout,
|
|
1868
|
+
disabled: loading,
|
|
1869
|
+
}, loading ? 'Redirecting to Stripe...' : 'Proceed to Checkout'),
|
|
1870
|
+
error ? h('p', { className: 'text-red-500 text-sm text-center' }, error) : null
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1430
1874
|
// --- Main App ---
|
|
1431
1875
|
function CatalogPreview({ catalog }) {
|
|
1432
1876
|
const pages = catalog.pages || {};
|
|
@@ -1435,13 +1879,68 @@ function buildPreviewHtml(schema, port) {
|
|
|
1435
1879
|
const [currentPageId, setCurrentPageId] = React.useState(routing.entry || pageKeys[0] || null);
|
|
1436
1880
|
const [formState, setFormState] = React.useState({});
|
|
1437
1881
|
const [history, setHistory] = React.useState([]);
|
|
1882
|
+
const [cartItems, setCartItems] = React.useState([]);
|
|
1883
|
+
const [cartOpen, setCartOpen] = React.useState(false);
|
|
1884
|
+
|
|
1885
|
+
// --- Cart logic ---
|
|
1886
|
+
const addToCart = React.useCallback((pageId) => {
|
|
1887
|
+
const pg = pages[pageId];
|
|
1888
|
+
if (!pg?.offer) return;
|
|
1889
|
+
const offer = pg.offer;
|
|
1890
|
+
setCartItems(prev => {
|
|
1891
|
+
if (prev.some(item => item.offer_id === offer.id)) return prev;
|
|
1892
|
+
return [...prev, {
|
|
1893
|
+
offer_id: offer.id,
|
|
1894
|
+
page_id: pageId,
|
|
1895
|
+
title: offer.title || pageId,
|
|
1896
|
+
price_display: offer.price_display,
|
|
1897
|
+
price_subtext: offer.price_subtext,
|
|
1898
|
+
image: offer.image,
|
|
1899
|
+
}];
|
|
1900
|
+
});
|
|
1901
|
+
}, [pages]);
|
|
1902
|
+
|
|
1903
|
+
const removeFromCart = React.useCallback((offerId) => {
|
|
1904
|
+
setCartItems(prev => prev.filter(item => item.offer_id !== offerId));
|
|
1905
|
+
// Clear accept_field so it doesn't re-add
|
|
1906
|
+
for (const [pid, pg] of Object.entries(pages)) {
|
|
1907
|
+
if (pg.offer?.id === offerId && pg.offer.accept_field) {
|
|
1908
|
+
setFormState(prev => { const next = { ...prev }; delete next[pg.offer.accept_field]; return next; });
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}, [pages]);
|
|
1912
|
+
|
|
1913
|
+
const toggleCart = React.useCallback(() => setCartOpen(prev => !prev), []);
|
|
1438
1914
|
|
|
1439
|
-
//
|
|
1915
|
+
// Detect offer acceptance from field changes
|
|
1916
|
+
React.useEffect(() => {
|
|
1917
|
+
for (const [pageId, pg] of Object.entries(pages)) {
|
|
1918
|
+
const offer = pg.offer;
|
|
1919
|
+
if (!offer?.accept_field) continue;
|
|
1920
|
+
const fieldValue = formState[offer.accept_field];
|
|
1921
|
+
const acceptValue = offer.accept_value || 'accept';
|
|
1922
|
+
if (fieldValue === acceptValue) {
|
|
1923
|
+
if (!cartItems.some(item => item.offer_id === offer.id)) addToCart(pageId);
|
|
1924
|
+
} else {
|
|
1925
|
+
if (cartItems.some(item => item.offer_id === offer.id) && fieldValue !== undefined) removeFromCart(offer.id);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}, [formState, pages, cartItems, addToCart, removeFromCart]);
|
|
1929
|
+
|
|
1930
|
+
// Expose navigation for mindmap + emit page_view
|
|
1440
1931
|
React.useEffect(() => {
|
|
1441
1932
|
window.__devNavigateTo = (id) => { setCurrentPageId(id); setHistory([]); window.scrollTo({ top: 0, behavior: 'smooth' }); };
|
|
1442
1933
|
window.__devSetCurrentPage && window.__devSetCurrentPage(currentPageId);
|
|
1934
|
+
devEvents.emit('page_view', { page_id: currentPageId, catalog_slug: catalog.slug });
|
|
1443
1935
|
}, [currentPageId]);
|
|
1444
1936
|
|
|
1937
|
+
// Expose debug state
|
|
1938
|
+
React.useEffect(() => {
|
|
1939
|
+
const edges = (routing.edges || []).filter(e => e.from === currentPageId);
|
|
1940
|
+
window.__devDebugState = { currentPageId, formState, cartItems, edges };
|
|
1941
|
+
window.dispatchEvent(new CustomEvent('devStateUpdate'));
|
|
1942
|
+
}, [currentPageId, formState, cartItems, routing]);
|
|
1943
|
+
|
|
1445
1944
|
const page = currentPageId ? pages[currentPageId] : null;
|
|
1446
1945
|
const isCover = page?.layout === 'cover';
|
|
1447
1946
|
const isLastPage = (() => {
|
|
@@ -1451,16 +1950,26 @@ function buildPreviewHtml(schema, port) {
|
|
|
1451
1950
|
|
|
1452
1951
|
const onFieldChange = React.useCallback((id, value) => {
|
|
1453
1952
|
setFormState(prev => ({ ...prev, [id]: value }));
|
|
1454
|
-
|
|
1953
|
+
devEvents.emit('field_change', { field_id: id, value, page_id: currentPageId });
|
|
1954
|
+
}, [currentPageId]);
|
|
1455
1955
|
|
|
1456
1956
|
const handleNext = React.useCallback(() => {
|
|
1957
|
+
// Check if page has an offer \u2014 treat "Next" as an accept action
|
|
1958
|
+
const currentPage = pages[currentPageId];
|
|
1959
|
+
if (currentPage?.offer) {
|
|
1960
|
+
const acceptValue = currentPage.offer.accept_value || 'accept';
|
|
1961
|
+
// If there's no accept_field, the CTA button itself is the accept trigger
|
|
1962
|
+
if (!currentPage.offer.accept_field) {
|
|
1963
|
+
addToCart(currentPageId);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1457
1966
|
const nextId = getNextPageId(currentPageId, routing, formState);
|
|
1458
1967
|
if (nextId && pages[nextId]) {
|
|
1459
1968
|
setHistory(prev => [...prev, currentPageId]);
|
|
1460
1969
|
setCurrentPageId(nextId);
|
|
1461
1970
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1462
1971
|
}
|
|
1463
|
-
}, [currentPageId, routing, formState, pages]);
|
|
1972
|
+
}, [currentPageId, routing, formState, pages, addToCart]);
|
|
1464
1973
|
|
|
1465
1974
|
const handleBack = React.useCallback(() => {
|
|
1466
1975
|
if (history.length > 0) {
|
|
@@ -1477,12 +1986,23 @@ function buildPreviewHtml(schema, port) {
|
|
|
1477
1986
|
);
|
|
1478
1987
|
}
|
|
1479
1988
|
|
|
1480
|
-
const components = (page.components || []).filter(c =>
|
|
1989
|
+
const components = (page.components || []).filter(c => {
|
|
1990
|
+
if (c.hidden || c.props?.hidden) return false;
|
|
1991
|
+
if (c.visibility) return evaluateConditionGroup(c.visibility, formState, devContext);
|
|
1992
|
+
return true;
|
|
1993
|
+
});
|
|
1481
1994
|
const bgImage = page.background_image || catalog.settings?.theme?.background_image;
|
|
1482
1995
|
|
|
1996
|
+
// Cart UI (shared between cover and standard)
|
|
1997
|
+
const cartUI = h(React.Fragment, null,
|
|
1998
|
+
h(CartButton, { itemCount: cartItems.length, onClick: toggleCart }),
|
|
1999
|
+
h(CartDrawer, { items: cartItems, isOpen: cartOpen, onToggle: toggleCart, onRemove: removeFromCart })
|
|
2000
|
+
);
|
|
2001
|
+
|
|
1483
2002
|
// Cover page layout
|
|
1484
2003
|
if (isCover) {
|
|
1485
2004
|
return h('div', { 'data-page-id': currentPageId },
|
|
2005
|
+
cartUI,
|
|
1486
2006
|
h('div', {
|
|
1487
2007
|
className: 'cf-page cf-noise min-h-screen flex items-center justify-center relative overflow-hidden',
|
|
1488
2008
|
style: {
|
|
@@ -1516,6 +2036,7 @@ function buildPreviewHtml(schema, port) {
|
|
|
1516
2036
|
const topBarEnabled = topBar?.enabled !== false && catalog.settings?.top_bar;
|
|
1517
2037
|
|
|
1518
2038
|
return h('div', { 'data-page-id': currentPageId },
|
|
2039
|
+
cartUI,
|
|
1519
2040
|
h('div', {
|
|
1520
2041
|
className: 'cf-page min-h-screen',
|
|
1521
2042
|
style: { background: 'linear-gradient(180deg, #f8f9fc 0%, #f0f2f7 100%)' },
|
|
@@ -1819,18 +2340,108 @@ function buildPreviewHtml(schema, port) {
|
|
|
1819
2340
|
});
|
|
1820
2341
|
}, true);
|
|
1821
2342
|
})();
|
|
2343
|
+
|
|
2344
|
+
// --- Validation banner ---
|
|
2345
|
+
(function initValidationBanner() {
|
|
2346
|
+
const banner = document.getElementById('validation-banner');
|
|
2347
|
+
const validation = JSON.parse(document.getElementById('__validation_data').textContent);
|
|
2348
|
+
const errors = validation.errors || [];
|
|
2349
|
+
const warnings = validation.warnings || [];
|
|
2350
|
+
if (errors.length === 0 && warnings.length === 0) return;
|
|
2351
|
+
let html = '<div class="vb-header"><strong>' + errors.length + ' error(s), ' + warnings.length + ' warning(s)</strong><button class="vb-dismiss" id="vb-dismiss">Dismiss</button></div>';
|
|
2352
|
+
for (const e of errors) html += '<div class="vb-error">ERROR: ' + e + '</div>';
|
|
2353
|
+
for (const w of warnings) html += '<div class="vb-warn">WARN: ' + w + '</div>';
|
|
2354
|
+
banner.innerHTML = html;
|
|
2355
|
+
banner.style.display = 'block';
|
|
2356
|
+
document.getElementById('vb-dismiss').addEventListener('click', () => { banner.style.display = 'none'; });
|
|
2357
|
+
})();
|
|
2358
|
+
|
|
2359
|
+
// --- Debug panel ---
|
|
2360
|
+
(function initDebugPanel() {
|
|
2361
|
+
const panel = document.getElementById('debug-panel');
|
|
2362
|
+
const body = document.getElementById('debug-body');
|
|
2363
|
+
const btn = document.getElementById('debug-btn');
|
|
2364
|
+
const closeBtn = document.getElementById('debug-close');
|
|
2365
|
+
let isOpen = false;
|
|
2366
|
+
|
|
2367
|
+
function toggle() {
|
|
2368
|
+
isOpen = !isOpen;
|
|
2369
|
+
panel.classList.toggle('open', isOpen);
|
|
2370
|
+
if (isOpen) render();
|
|
2371
|
+
}
|
|
2372
|
+
btn.addEventListener('click', toggle);
|
|
2373
|
+
closeBtn.addEventListener('click', toggle);
|
|
2374
|
+
|
|
2375
|
+
document.addEventListener('keydown', (e) => {
|
|
2376
|
+
if (e.ctrlKey && e.key === 'd') { e.preventDefault(); toggle(); }
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
const recentEvents = [];
|
|
2380
|
+
window.addEventListener('devEvent', (e) => {
|
|
2381
|
+
recentEvents.push(e.detail);
|
|
2382
|
+
if (recentEvents.length > 20) recentEvents.shift();
|
|
2383
|
+
if (isOpen) render();
|
|
2384
|
+
});
|
|
2385
|
+
|
|
2386
|
+
function render() {
|
|
2387
|
+
const state = window.__devDebugState;
|
|
2388
|
+
if (!state) { body.innerHTML = '<p>Waiting for state...</p>'; return; }
|
|
2389
|
+
let html = '<div class="dp-section"><div class="dp-label">Current Page</div><span class="dp-badge">' + (state.currentPageId || 'none') + '</span></div>';
|
|
2390
|
+
html += '<div class="dp-section"><div class="dp-label">Form State</div><pre>' + JSON.stringify(state.formState || {}, null, 2) + '</pre></div>';
|
|
2391
|
+
html += '<div class="dp-section"><div class="dp-label">Cart (' + (state.cartItems?.length || 0) + ')</div>';
|
|
2392
|
+
if (state.cartItems && state.cartItems.length > 0) {
|
|
2393
|
+
html += '<pre>' + JSON.stringify(state.cartItems.map(i => i.title || i.offer_id), null, 2) + '</pre>';
|
|
2394
|
+
} else {
|
|
2395
|
+
html += '<pre>empty</pre>';
|
|
2396
|
+
}
|
|
2397
|
+
html += '</div>';
|
|
2398
|
+
html += '<div class="dp-section"><div class="dp-label">Edges from here</div>';
|
|
2399
|
+
if (state.edges && state.edges.length > 0) {
|
|
2400
|
+
html += '<pre>' + state.edges.map(e => e.from + ' \u2192 ' + e.to + (e.conditions?.length ? ' (conditional)' : '')).join('\\n') + '</pre>';
|
|
2401
|
+
} else {
|
|
2402
|
+
html += '<pre>none (terminal page)</pre>';
|
|
2403
|
+
}
|
|
2404
|
+
html += '</div>';
|
|
2405
|
+
html += '<div class="dp-section"><div class="dp-label">Recent Events (' + recentEvents.length + ')</div>';
|
|
2406
|
+
if (recentEvents.length > 0) {
|
|
2407
|
+
html += '<pre>' + recentEvents.slice(-8).map(e => e.type + ' ' + JSON.stringify(e.data || {})).join('\\n') + '</pre>';
|
|
2408
|
+
} else {
|
|
2409
|
+
html += '<pre>none yet</pre>';
|
|
2410
|
+
}
|
|
2411
|
+
html += '</div>';
|
|
2412
|
+
body.innerHTML = html;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
window.addEventListener('devStateUpdate', () => { if (isOpen) render(); });
|
|
2416
|
+
})();
|
|
1822
2417
|
</script>
|
|
1823
2418
|
</body>
|
|
1824
2419
|
</html>`;
|
|
1825
2420
|
}
|
|
1826
2421
|
async function catalogDev(file, opts) {
|
|
1827
|
-
const abs =
|
|
2422
|
+
const abs = resolve4(file);
|
|
1828
2423
|
const catalogDir = dirname2(abs);
|
|
1829
2424
|
const port = parseInt(opts.port || String(DEFAULT_PORT), 10);
|
|
1830
2425
|
if (!existsSync2(abs)) {
|
|
1831
2426
|
console.error(`File not found: ${file}`);
|
|
1832
2427
|
process.exit(1);
|
|
1833
2428
|
}
|
|
2429
|
+
let stripeSecretKey = process.env.STRIPE_SECRET_KEY || "";
|
|
2430
|
+
if (!stripeSecretKey) {
|
|
2431
|
+
for (const envFile of [".env", ".env.local", ".env.development"]) {
|
|
2432
|
+
const envPath = join2(catalogDir, envFile);
|
|
2433
|
+
if (existsSync2(envPath)) {
|
|
2434
|
+
const envContent = readFileSync4(envPath, "utf-8");
|
|
2435
|
+
const match = envContent.match(/^STRIPE_SECRET_KEY\s*=\s*"?([^"\n]+)"?/m);
|
|
2436
|
+
if (match) {
|
|
2437
|
+
stripeSecretKey = match[1].trim();
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
const stripeEnabled = stripeSecretKey.length > 0;
|
|
2444
|
+
const devConfig = { stripeEnabled, port };
|
|
1834
2445
|
const spinner = ora5("Loading catalog schema...").start();
|
|
1835
2446
|
let schema;
|
|
1836
2447
|
try {
|
|
@@ -1839,14 +2450,15 @@ async function catalogDev(file, opts) {
|
|
|
1839
2450
|
spinner.fail(`Failed to load catalog: ${err.message}`);
|
|
1840
2451
|
process.exit(1);
|
|
1841
2452
|
}
|
|
1842
|
-
const
|
|
1843
|
-
if (
|
|
2453
|
+
const basicErrors = validateCatalog(schema);
|
|
2454
|
+
if (basicErrors.length > 0) {
|
|
1844
2455
|
spinner.fail("Schema validation errors:");
|
|
1845
|
-
for (const err of
|
|
2456
|
+
for (const err of basicErrors) {
|
|
1846
2457
|
console.error(` - ${err}`);
|
|
1847
2458
|
}
|
|
1848
2459
|
process.exit(1);
|
|
1849
2460
|
}
|
|
2461
|
+
let validation = deepValidateCatalog(schema);
|
|
1850
2462
|
const localBaseUrl = `http://localhost:${port}/assets`;
|
|
1851
2463
|
schema = resolveLocalAssets(schema, catalogDir, localBaseUrl);
|
|
1852
2464
|
spinner.succeed(`Loaded: ${schema.slug || file}`);
|
|
@@ -1854,6 +2466,8 @@ async function catalogDev(file, opts) {
|
|
|
1854
2466
|
console.log(` Entry: ${schema.routing?.entry || "first page"}`);
|
|
1855
2467
|
console.log();
|
|
1856
2468
|
const sseClients = /* @__PURE__ */ new Set();
|
|
2469
|
+
const eventSseClients = /* @__PURE__ */ new Set();
|
|
2470
|
+
const eventLog = [];
|
|
1857
2471
|
function notifyReload() {
|
|
1858
2472
|
sseClients.forEach((client) => {
|
|
1859
2473
|
try {
|
|
@@ -1863,6 +2477,22 @@ async function catalogDev(file, opts) {
|
|
|
1863
2477
|
}
|
|
1864
2478
|
});
|
|
1865
2479
|
}
|
|
2480
|
+
function emitDevEvent(event) {
|
|
2481
|
+
eventLog.push(event);
|
|
2482
|
+
if (eventLog.length > 500) eventLog.shift();
|
|
2483
|
+
const data = JSON.stringify(event);
|
|
2484
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
2485
|
+
console.log(` \x1B[36m[event]\x1B[0m ${ts} ${event.type} ${JSON.stringify(event.data || {})}`);
|
|
2486
|
+
eventSseClients.forEach((client) => {
|
|
2487
|
+
try {
|
|
2488
|
+
client.write(`data: ${data}
|
|
2489
|
+
|
|
2490
|
+
`);
|
|
2491
|
+
} catch {
|
|
2492
|
+
eventSseClients.delete(client);
|
|
2493
|
+
}
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
1866
2496
|
const server = createServer(async (req, res) => {
|
|
1867
2497
|
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
1868
2498
|
if (url.pathname === "/__dev_sse") {
|
|
@@ -1877,11 +2507,127 @@ async function catalogDev(file, opts) {
|
|
|
1877
2507
|
req.on("close", () => sseClients.delete(res));
|
|
1878
2508
|
return;
|
|
1879
2509
|
}
|
|
2510
|
+
if (url.pathname === "/__dev_events_stream") {
|
|
2511
|
+
res.writeHead(200, {
|
|
2512
|
+
"Content-Type": "text/event-stream",
|
|
2513
|
+
"Cache-Control": "no-cache",
|
|
2514
|
+
"Connection": "keep-alive",
|
|
2515
|
+
"Access-Control-Allow-Origin": "*"
|
|
2516
|
+
});
|
|
2517
|
+
res.write("data: connected\n\n");
|
|
2518
|
+
eventSseClients.add(res);
|
|
2519
|
+
req.on("close", () => eventSseClients.delete(res));
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
if (url.pathname === "/__dev_events" && req.method === "GET") {
|
|
2523
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
2524
|
+
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
2525
|
+
res.end(JSON.stringify(eventLog.slice(-limit)));
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
if (url.pathname === "/__dev_event" && req.method === "POST") {
|
|
2529
|
+
let body = "";
|
|
2530
|
+
req.on("data", (chunk) => {
|
|
2531
|
+
body += chunk;
|
|
2532
|
+
});
|
|
2533
|
+
req.on("end", () => {
|
|
2534
|
+
try {
|
|
2535
|
+
const event = JSON.parse(body);
|
|
2536
|
+
emitDevEvent(event);
|
|
2537
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
2538
|
+
res.end('{"ok":true}');
|
|
2539
|
+
} catch {
|
|
2540
|
+
res.writeHead(400);
|
|
2541
|
+
res.end('{"error":"invalid json"}');
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
if (url.pathname === "/__dev_checkout" && req.method === "POST") {
|
|
2547
|
+
let body = "";
|
|
2548
|
+
req.on("data", (chunk) => {
|
|
2549
|
+
body += chunk;
|
|
2550
|
+
});
|
|
2551
|
+
req.on("end", async () => {
|
|
2552
|
+
res.setHeader("Content-Type", "application/json");
|
|
2553
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2554
|
+
if (!stripeEnabled) {
|
|
2555
|
+
res.writeHead(400);
|
|
2556
|
+
res.end(JSON.stringify({ error: "No STRIPE_SECRET_KEY found. Add it to .env in your catalog directory." }));
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
try {
|
|
2560
|
+
const { line_items, form_state, catalog_slug } = JSON.parse(body);
|
|
2561
|
+
const checkoutSettings = schema.settings?.checkout || {};
|
|
2562
|
+
const params = new URLSearchParams();
|
|
2563
|
+
params.set("mode", checkoutSettings.payment_type === "subscription" ? "subscription" : "payment");
|
|
2564
|
+
params.set("success_url", `http://localhost:${port}/?checkout=success&session_id={CHECKOUT_SESSION_ID}`);
|
|
2565
|
+
params.set("cancel_url", `http://localhost:${port}/?checkout=cancel`);
|
|
2566
|
+
const emailField = checkoutSettings.prefill_fields?.customer_email;
|
|
2567
|
+
if (emailField && form_state?.[emailField]) {
|
|
2568
|
+
params.set("customer_email", form_state[emailField]);
|
|
2569
|
+
}
|
|
2570
|
+
const methods = checkoutSettings.payment_methods || ["card"];
|
|
2571
|
+
methods.forEach((m, i) => params.set(`payment_method_types[${i}]`, m));
|
|
2572
|
+
if (checkoutSettings.allow_discount_codes) {
|
|
2573
|
+
params.set("allow_promotion_codes", "true");
|
|
2574
|
+
}
|
|
2575
|
+
line_items.forEach((item, i) => {
|
|
2576
|
+
if (item.stripe_price_id) {
|
|
2577
|
+
params.set(`line_items[${i}][price]`, item.stripe_price_id);
|
|
2578
|
+
params.set(`line_items[${i}][quantity]`, String(item.quantity || 1));
|
|
2579
|
+
} else if (item.amount_cents) {
|
|
2580
|
+
params.set(`line_items[${i}][price_data][currency]`, item.currency || "usd");
|
|
2581
|
+
params.set(`line_items[${i}][price_data][unit_amount]`, String(item.amount_cents));
|
|
2582
|
+
params.set(`line_items[${i}][price_data][product_data][name]`, item.title || "Item");
|
|
2583
|
+
if (checkoutSettings.payment_type === "subscription") {
|
|
2584
|
+
params.set(`line_items[${i}][price_data][recurring][interval]`, "month");
|
|
2585
|
+
}
|
|
2586
|
+
params.set(`line_items[${i}][quantity]`, String(item.quantity || 1));
|
|
2587
|
+
}
|
|
2588
|
+
});
|
|
2589
|
+
if (checkoutSettings.payment_type === "subscription" && checkoutSettings.free_trial?.enabled && checkoutSettings.free_trial.days) {
|
|
2590
|
+
params.set("subscription_data[trial_period_days]", String(checkoutSettings.free_trial.days));
|
|
2591
|
+
}
|
|
2592
|
+
const stripeRes = await fetch("https://api.stripe.com/v1/checkout/sessions", {
|
|
2593
|
+
method: "POST",
|
|
2594
|
+
headers: {
|
|
2595
|
+
"Authorization": `Basic ${Buffer.from(stripeSecretKey + ":").toString("base64")}`,
|
|
2596
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2597
|
+
},
|
|
2598
|
+
body: params.toString()
|
|
2599
|
+
});
|
|
2600
|
+
const stripeData = await stripeRes.json();
|
|
2601
|
+
if (!stripeRes.ok) {
|
|
2602
|
+
emitDevEvent({ type: "checkout_error", timestamp: (/* @__PURE__ */ new Date()).toISOString(), data: { error: stripeData.error?.message || "Stripe error" } });
|
|
2603
|
+
res.writeHead(stripeRes.status);
|
|
2604
|
+
res.end(JSON.stringify({ error: stripeData.error?.message || "Stripe session creation failed" }));
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
emitDevEvent({ type: "checkout_session_created", timestamp: (/* @__PURE__ */ new Date()).toISOString(), data: { session_id: stripeData.id, url: stripeData.url } });
|
|
2608
|
+
res.writeHead(200);
|
|
2609
|
+
res.end(JSON.stringify({ session_id: stripeData.id, session_url: stripeData.url }));
|
|
2610
|
+
} catch (err) {
|
|
2611
|
+
res.writeHead(500);
|
|
2612
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
if (req.method === "OPTIONS") {
|
|
2618
|
+
res.writeHead(204, {
|
|
2619
|
+
"Access-Control-Allow-Origin": "*",
|
|
2620
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
2621
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
2622
|
+
});
|
|
2623
|
+
res.end();
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
1880
2626
|
if (url.pathname.startsWith("/assets/")) {
|
|
1881
2627
|
const relativePath = decodeURIComponent(url.pathname.slice("/assets/".length));
|
|
1882
2628
|
const filePath = join2(catalogDir, relativePath);
|
|
1883
|
-
const resolved =
|
|
1884
|
-
if (!resolved.startsWith(
|
|
2629
|
+
const resolved = resolve4(filePath);
|
|
2630
|
+
if (!resolved.startsWith(resolve4(catalogDir))) {
|
|
1885
2631
|
res.writeHead(403);
|
|
1886
2632
|
res.end("Forbidden");
|
|
1887
2633
|
return;
|
|
@@ -1909,11 +2655,13 @@ async function catalogDev(file, opts) {
|
|
|
1909
2655
|
"Content-Type": "text/html; charset=utf-8",
|
|
1910
2656
|
"Cache-Control": "no-cache"
|
|
1911
2657
|
});
|
|
1912
|
-
res.end(buildPreviewHtml(schema, port));
|
|
2658
|
+
res.end(buildPreviewHtml(schema, port, validation, devConfig));
|
|
1913
2659
|
});
|
|
1914
2660
|
server.listen(port, () => {
|
|
1915
2661
|
console.log(` Local preview: http://localhost:${port}`);
|
|
1916
2662
|
console.log(` Assets served from: ${catalogDir}`);
|
|
2663
|
+
console.log(` Checkout: ${stripeEnabled ? `\x1B[32menabled\x1B[0m (${stripeSecretKey.startsWith("sk_test_") ? "test mode" : "live mode"})` : "\x1B[33mstubbed\x1B[0m \u2014 add STRIPE_SECRET_KEY to .env"}`);
|
|
2664
|
+
console.log(` Events: \x1B[32mlocal\x1B[0m \u2014 stream at http://localhost:${port}/__dev_events_stream`);
|
|
1917
2665
|
console.log(` Watching for changes...
|
|
1918
2666
|
`);
|
|
1919
2667
|
});
|
|
@@ -1930,6 +2678,7 @@ async function catalogDev(file, opts) {
|
|
|
1930
2678
|
for (const e of errs) console.error(` - ${e}`);
|
|
1931
2679
|
return;
|
|
1932
2680
|
}
|
|
2681
|
+
validation = deepValidateCatalog(schema);
|
|
1933
2682
|
schema = resolveLocalAssets(schema, catalogDir, localBaseUrl);
|
|
1934
2683
|
reloadSpinner.succeed(`Reloaded \u2014 auto-refreshing browser`);
|
|
1935
2684
|
notifyReload();
|
|
@@ -1940,7 +2689,7 @@ async function catalogDev(file, opts) {
|
|
|
1940
2689
|
});
|
|
1941
2690
|
try {
|
|
1942
2691
|
watch(catalogDir, { recursive: true }, (event, filename) => {
|
|
1943
|
-
if (!filename || filename.startsWith(".") ||
|
|
2692
|
+
if (!filename || filename.startsWith(".") || resolve4(join2(catalogDir, filename)) === abs) return;
|
|
1944
2693
|
});
|
|
1945
2694
|
} catch {
|
|
1946
2695
|
}
|
|
@@ -1951,6 +2700,439 @@ async function catalogDev(file, opts) {
|
|
|
1951
2700
|
});
|
|
1952
2701
|
}
|
|
1953
2702
|
|
|
2703
|
+
// src/commands/catalog-validate.ts
|
|
2704
|
+
import { resolve as resolve5 } from "path";
|
|
2705
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2706
|
+
import ora6 from "ora";
|
|
2707
|
+
async function catalogValidate(file) {
|
|
2708
|
+
const abs = resolve5(file);
|
|
2709
|
+
if (!existsSync3(abs)) {
|
|
2710
|
+
console.error(`File not found: ${file}`);
|
|
2711
|
+
process.exit(1);
|
|
2712
|
+
}
|
|
2713
|
+
const spinner = ora6("Loading catalog schema...").start();
|
|
2714
|
+
let schema;
|
|
2715
|
+
try {
|
|
2716
|
+
schema = await loadCatalogFile(abs);
|
|
2717
|
+
} catch (err) {
|
|
2718
|
+
spinner.fail(`Failed to load catalog: ${err.message}`);
|
|
2719
|
+
process.exit(1);
|
|
2720
|
+
}
|
|
2721
|
+
spinner.succeed(`Loaded: ${schema.slug || file}`);
|
|
2722
|
+
const basicErrors = validateCatalog(schema);
|
|
2723
|
+
const { errors: deepErrors, warnings } = deepValidateCatalog(schema);
|
|
2724
|
+
const allErrors = [...basicErrors, ...deepErrors];
|
|
2725
|
+
const pages = schema.pages || {};
|
|
2726
|
+
const pageCount = Object.keys(pages).length;
|
|
2727
|
+
const componentCount = Object.values(pages).reduce(
|
|
2728
|
+
(sum, p) => sum + (p.components?.length || 0),
|
|
2729
|
+
0
|
|
2730
|
+
);
|
|
2731
|
+
const edgeCount = schema.routing?.edges?.length || 0;
|
|
2732
|
+
const offerCount = Object.values(pages).filter((p) => p.offer).length;
|
|
2733
|
+
console.log();
|
|
2734
|
+
console.log(` Slug: ${schema.slug || "(none)"}`);
|
|
2735
|
+
console.log(` Version: ${schema.schema_version || "(none)"}`);
|
|
2736
|
+
console.log(` Pages: ${pageCount}`);
|
|
2737
|
+
console.log(` Components: ${componentCount}`);
|
|
2738
|
+
console.log(` Edges: ${edgeCount}`);
|
|
2739
|
+
console.log(` Offers: ${offerCount}`);
|
|
2740
|
+
console.log();
|
|
2741
|
+
if (allErrors.length > 0) {
|
|
2742
|
+
for (const err of allErrors) {
|
|
2743
|
+
console.error(` \x1B[31mERROR\x1B[0m ${err}`);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
if (warnings.length > 0) {
|
|
2747
|
+
for (const warn of warnings) {
|
|
2748
|
+
console.warn(` \x1B[33mWARN\x1B[0m ${warn}`);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
if (allErrors.length === 0 && warnings.length === 0) {
|
|
2752
|
+
console.log(" \x1B[32m\u2713 No issues found\x1B[0m");
|
|
2753
|
+
} else {
|
|
2754
|
+
console.log();
|
|
2755
|
+
console.log(
|
|
2756
|
+
` ${allErrors.length} error(s), ${warnings.length} warning(s)`
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
process.exit(allErrors.length > 0 ? 1 : 0);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// src/commands/catalog-init.ts
|
|
2763
|
+
import { mkdirSync, writeFileSync, existsSync as existsSync4 } from "fs";
|
|
2764
|
+
import { join as join3 } from "path";
|
|
2765
|
+
import { createInterface } from "readline";
|
|
2766
|
+
function ask(rl, question) {
|
|
2767
|
+
return new Promise((resolve7) => rl.question(question, resolve7));
|
|
2768
|
+
}
|
|
2769
|
+
var TEMPLATES = {
|
|
2770
|
+
"quiz-funnel": {
|
|
2771
|
+
description: "3-page quiz funnel: welcome cover, quiz question, result with offer",
|
|
2772
|
+
content: (slug) => `import type { CatalogSchema } from "@shared/types";
|
|
2773
|
+
|
|
2774
|
+
const catalog = {
|
|
2775
|
+
schema_version: "1.0",
|
|
2776
|
+
slug: "${slug}",
|
|
2777
|
+
settings: {
|
|
2778
|
+
theme: { primary_color: "#6366f1" },
|
|
2779
|
+
progress_steps: [
|
|
2780
|
+
{ id: "quiz", label: "Quiz", pages: ["welcome", "question"] },
|
|
2781
|
+
{ id: "result", label: "Result", pages: ["result"] },
|
|
2782
|
+
],
|
|
2783
|
+
},
|
|
2784
|
+
routing: {
|
|
2785
|
+
entry: "welcome",
|
|
2786
|
+
edges: [
|
|
2787
|
+
{ from: "welcome", to: "question" },
|
|
2788
|
+
{ from: "question", to: "result" },
|
|
2789
|
+
],
|
|
2790
|
+
},
|
|
2791
|
+
pages: {
|
|
2792
|
+
welcome: {
|
|
2793
|
+
layout: "cover",
|
|
2794
|
+
components: [
|
|
2795
|
+
{ id: "heading", type: "heading", props: { level: 1, text: "Find Your Perfect Plan", subtitle: "Answer a few quick questions to get a personalized recommendation." } },
|
|
2796
|
+
],
|
|
2797
|
+
},
|
|
2798
|
+
question: {
|
|
2799
|
+
title: "About You",
|
|
2800
|
+
components: [
|
|
2801
|
+
{ id: "role", type: "multiple_choice", props: { label: "What best describes you?", options: ["Solopreneur", "Agency", "E-commerce", "SaaS"], required: true } },
|
|
2802
|
+
],
|
|
2803
|
+
},
|
|
2804
|
+
result: {
|
|
2805
|
+
title: "Your Result",
|
|
2806
|
+
components: [
|
|
2807
|
+
{ id: "result_heading", type: "heading", props: { level: 2, text: "Here's our recommendation" } },
|
|
2808
|
+
{ id: "result_text", type: "paragraph", props: { text: "Based on your answers, we think this plan is perfect for you." } },
|
|
2809
|
+
],
|
|
2810
|
+
offer: {
|
|
2811
|
+
id: "main-offer",
|
|
2812
|
+
title: "Starter Plan",
|
|
2813
|
+
price_display: "$29/mo",
|
|
2814
|
+
},
|
|
2815
|
+
},
|
|
2816
|
+
},
|
|
2817
|
+
} satisfies CatalogSchema;
|
|
2818
|
+
|
|
2819
|
+
export default catalog;
|
|
2820
|
+
`
|
|
2821
|
+
},
|
|
2822
|
+
"lead-capture": {
|
|
2823
|
+
description: "2-page lead capture: info form + thank you",
|
|
2824
|
+
content: (slug) => `import type { CatalogSchema } from "@shared/types";
|
|
2825
|
+
|
|
2826
|
+
const catalog = {
|
|
2827
|
+
schema_version: "1.0",
|
|
2828
|
+
slug: "${slug}",
|
|
2829
|
+
settings: {
|
|
2830
|
+
theme: { primary_color: "#10b981" },
|
|
2831
|
+
},
|
|
2832
|
+
routing: {
|
|
2833
|
+
entry: "form",
|
|
2834
|
+
edges: [
|
|
2835
|
+
{ from: "form", to: "thank_you" },
|
|
2836
|
+
],
|
|
2837
|
+
},
|
|
2838
|
+
pages: {
|
|
2839
|
+
form: {
|
|
2840
|
+
title: "Get Started",
|
|
2841
|
+
components: [
|
|
2842
|
+
{ id: "heading", type: "heading", props: { level: 2, text: "Tell us about yourself" } },
|
|
2843
|
+
{ id: "name", type: "short_text", props: { label: "Full Name", placeholder: "Jane Doe", required: true } },
|
|
2844
|
+
{ id: "email", type: "email", props: { label: "Email", placeholder: "you@example.com", required: true } },
|
|
2845
|
+
{ id: "company", type: "short_text", props: { label: "Company", placeholder: "Acme Inc" } },
|
|
2846
|
+
],
|
|
2847
|
+
},
|
|
2848
|
+
thank_you: {
|
|
2849
|
+
title: "Thank You!",
|
|
2850
|
+
hide_navigation: true,
|
|
2851
|
+
components: [
|
|
2852
|
+
{ id: "thanks", type: "heading", props: { level: 2, text: "Thanks! We'll be in touch soon." } },
|
|
2853
|
+
{ id: "note", type: "paragraph", props: { text: "Check your inbox for a confirmation email." } },
|
|
2854
|
+
],
|
|
2855
|
+
},
|
|
2856
|
+
},
|
|
2857
|
+
} satisfies CatalogSchema;
|
|
2858
|
+
|
|
2859
|
+
export default catalog;
|
|
2860
|
+
`
|
|
2861
|
+
},
|
|
2862
|
+
"product-catalog": {
|
|
2863
|
+
description: "2-page product showcase: pricing cards + checkout",
|
|
2864
|
+
content: (slug) => `import type { CatalogSchema } from "@shared/types";
|
|
2865
|
+
|
|
2866
|
+
const catalog = {
|
|
2867
|
+
schema_version: "1.0",
|
|
2868
|
+
slug: "${slug}",
|
|
2869
|
+
settings: {
|
|
2870
|
+
theme: { primary_color: "#f59e0b" },
|
|
2871
|
+
},
|
|
2872
|
+
routing: {
|
|
2873
|
+
entry: "pricing",
|
|
2874
|
+
edges: [
|
|
2875
|
+
{ from: "pricing", to: "checkout" },
|
|
2876
|
+
],
|
|
2877
|
+
},
|
|
2878
|
+
pages: {
|
|
2879
|
+
pricing: {
|
|
2880
|
+
title: "Choose a Plan",
|
|
2881
|
+
components: [
|
|
2882
|
+
{ id: "heading", type: "heading", props: { level: 1, text: "Simple, transparent pricing" } },
|
|
2883
|
+
{ id: "starter", type: "pricing_card", props: { title: "Starter", price: "$19", period: "mo", features: ["5 projects", "Basic analytics", "Email support"], cta_text: "Get Started" } },
|
|
2884
|
+
{ id: "pro", type: "pricing_card", props: { title: "Pro", badge: "Popular", price: "$49", period: "mo", features: ["Unlimited projects", "Advanced analytics", "Priority support", "Custom domain"], cta_text: "Go Pro" } },
|
|
2885
|
+
],
|
|
2886
|
+
},
|
|
2887
|
+
checkout: {
|
|
2888
|
+
title: "Complete Your Order",
|
|
2889
|
+
components: [
|
|
2890
|
+
{ id: "checkout_pay", type: "payment", props: { amount: 4900, currency: "usd" } },
|
|
2891
|
+
],
|
|
2892
|
+
},
|
|
2893
|
+
},
|
|
2894
|
+
} satisfies CatalogSchema;
|
|
2895
|
+
|
|
2896
|
+
export default catalog;
|
|
2897
|
+
`
|
|
2898
|
+
},
|
|
2899
|
+
blank: {
|
|
2900
|
+
description: "Minimal 1-page skeleton",
|
|
2901
|
+
content: (slug) => `import type { CatalogSchema } from "@shared/types";
|
|
2902
|
+
|
|
2903
|
+
const catalog = {
|
|
2904
|
+
schema_version: "1.0",
|
|
2905
|
+
slug: "${slug}",
|
|
2906
|
+
settings: {
|
|
2907
|
+
theme: { primary_color: "#6366f1" },
|
|
2908
|
+
},
|
|
2909
|
+
routing: {
|
|
2910
|
+
entry: "main",
|
|
2911
|
+
edges: [],
|
|
2912
|
+
},
|
|
2913
|
+
pages: {
|
|
2914
|
+
main: {
|
|
2915
|
+
title: "Welcome",
|
|
2916
|
+
components: [
|
|
2917
|
+
{ id: "heading", type: "heading", props: { level: 1, text: "Hello, World!" } },
|
|
2918
|
+
{ id: "text", type: "paragraph", props: { text: "Start building your catalog here." } },
|
|
2919
|
+
],
|
|
2920
|
+
},
|
|
2921
|
+
},
|
|
2922
|
+
} satisfies CatalogSchema;
|
|
2923
|
+
|
|
2924
|
+
export default catalog;
|
|
2925
|
+
`
|
|
2926
|
+
}
|
|
2927
|
+
};
|
|
2928
|
+
async function catalogInit() {
|
|
2929
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2930
|
+
try {
|
|
2931
|
+
console.log("\n Catalog Kit \u2014 New Catalog Scaffold\n");
|
|
2932
|
+
const slug = (await ask(rl, " Catalog slug (e.g. my-quiz): ")).trim();
|
|
2933
|
+
if (!slug || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
2934
|
+
console.error("\n Invalid slug. Use lowercase letters, numbers, and hyphens.");
|
|
2935
|
+
process.exit(1);
|
|
2936
|
+
}
|
|
2937
|
+
if (existsSync4(slug)) {
|
|
2938
|
+
console.error(`
|
|
2939
|
+
Directory "${slug}" already exists.`);
|
|
2940
|
+
process.exit(1);
|
|
2941
|
+
}
|
|
2942
|
+
console.log("\n Templates:");
|
|
2943
|
+
const templateKeys = Object.keys(TEMPLATES);
|
|
2944
|
+
for (let i = 0; i < templateKeys.length; i++) {
|
|
2945
|
+
const key = templateKeys[i];
|
|
2946
|
+
console.log(` ${i + 1}. ${key} \u2014 ${TEMPLATES[key].description}`);
|
|
2947
|
+
}
|
|
2948
|
+
const choice = (await ask(rl, `
|
|
2949
|
+
Pick a template (1-${templateKeys.length}): `)).trim();
|
|
2950
|
+
const idx = parseInt(choice, 10) - 1;
|
|
2951
|
+
if (isNaN(idx) || idx < 0 || idx >= templateKeys.length) {
|
|
2952
|
+
console.error("\n Invalid choice.");
|
|
2953
|
+
process.exit(1);
|
|
2954
|
+
}
|
|
2955
|
+
const templateKey = templateKeys[idx];
|
|
2956
|
+
const template = TEMPLATES[templateKey];
|
|
2957
|
+
mkdirSync(join3(slug, "images"), { recursive: true });
|
|
2958
|
+
writeFileSync(join3(slug, `${slug}.ts`), template.content(slug));
|
|
2959
|
+
writeFileSync(
|
|
2960
|
+
join3(slug, ".env.example"),
|
|
2961
|
+
`CATALOG_KIT_TOKEN=cfk_your_token_here
|
|
2962
|
+
`
|
|
2963
|
+
);
|
|
2964
|
+
console.log(`
|
|
2965
|
+
\x1B[32m\u2713 Created ${slug}/\x1B[0m`);
|
|
2966
|
+
console.log(` ${slug}/${slug}.ts`);
|
|
2967
|
+
console.log(` ${slug}/images/`);
|
|
2968
|
+
console.log(` ${slug}/.env.example`);
|
|
2969
|
+
console.log(`
|
|
2970
|
+
Next steps:`);
|
|
2971
|
+
console.log(` cd ${slug}`);
|
|
2972
|
+
console.log(` catalogs catalog dev ${slug}.ts`);
|
|
2973
|
+
console.log(` catalogs catalog validate ${slug}.ts`);
|
|
2974
|
+
console.log(` catalogs catalog push ${slug}.ts --publish
|
|
2975
|
+
`);
|
|
2976
|
+
} finally {
|
|
2977
|
+
rl.close();
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
// src/commands/catalog-diff.ts
|
|
2982
|
+
import { resolve as resolve6 } from "path";
|
|
2983
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2984
|
+
import ora7 from "ora";
|
|
2985
|
+
async function catalogDiff(file) {
|
|
2986
|
+
const config = requireConfig();
|
|
2987
|
+
const api = new ApiClient(config);
|
|
2988
|
+
await printIdentity(api);
|
|
2989
|
+
const abs = resolve6(file);
|
|
2990
|
+
if (!existsSync5(abs)) {
|
|
2991
|
+
console.error(`File not found: ${file}`);
|
|
2992
|
+
process.exit(1);
|
|
2993
|
+
}
|
|
2994
|
+
const spinner = ora7("Loading local catalog...").start();
|
|
2995
|
+
let local;
|
|
2996
|
+
try {
|
|
2997
|
+
local = await loadCatalogFile(abs);
|
|
2998
|
+
} catch (err) {
|
|
2999
|
+
spinner.fail(`Failed to load catalog: ${err.message}`);
|
|
3000
|
+
process.exit(1);
|
|
3001
|
+
}
|
|
3002
|
+
spinner.succeed(`Local: ${local.slug || file}`);
|
|
3003
|
+
const fetchSpinner = ora7("Fetching remote catalog...").start();
|
|
3004
|
+
let remote = null;
|
|
3005
|
+
try {
|
|
3006
|
+
const listRes = await api.get("/api/v1/catalogs");
|
|
3007
|
+
const catalogs = listRes.data || [];
|
|
3008
|
+
const match = catalogs.find((c) => c.slug === local.slug);
|
|
3009
|
+
if (match) {
|
|
3010
|
+
remote = match.schema;
|
|
3011
|
+
}
|
|
3012
|
+
} catch (err) {
|
|
3013
|
+
fetchSpinner.fail(`Failed to fetch remote: ${err.message}`);
|
|
3014
|
+
process.exit(1);
|
|
3015
|
+
}
|
|
3016
|
+
if (!remote) {
|
|
3017
|
+
fetchSpinner.info("This would be a new catalog (slug not found remotely).");
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
fetchSpinner.succeed(`Remote: ${remote.slug || local.slug}`);
|
|
3021
|
+
console.log();
|
|
3022
|
+
const localPages = new Set(Object.keys(local.pages || {}));
|
|
3023
|
+
const remotePages = new Set(Object.keys(remote.pages || {}));
|
|
3024
|
+
const addedPages = [];
|
|
3025
|
+
const removedPages = [];
|
|
3026
|
+
const modifiedPages = [];
|
|
3027
|
+
for (const id of localPages) {
|
|
3028
|
+
if (!remotePages.has(id)) {
|
|
3029
|
+
addedPages.push(id);
|
|
3030
|
+
} else {
|
|
3031
|
+
const localComps = (local.pages[id].components || []).map((c) => `${c.id}:${c.type}`).join(",");
|
|
3032
|
+
const remoteComps = (remote.pages[id].components || []).map((c) => `${c.id}:${c.type}`).join(",");
|
|
3033
|
+
if (localComps !== remoteComps) {
|
|
3034
|
+
modifiedPages.push(id);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
for (const id of remotePages) {
|
|
3039
|
+
if (!localPages.has(id)) {
|
|
3040
|
+
removedPages.push(id);
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
if (addedPages.length + removedPages.length + modifiedPages.length === 0) {
|
|
3044
|
+
console.log(" Pages: no changes");
|
|
3045
|
+
} else {
|
|
3046
|
+
console.log(" Pages:");
|
|
3047
|
+
for (const id of addedPages) {
|
|
3048
|
+
console.log(` \x1B[32m+ ${id}\x1B[0m`);
|
|
3049
|
+
}
|
|
3050
|
+
for (const id of removedPages) {
|
|
3051
|
+
console.log(` \x1B[31m- ${id}\x1B[0m`);
|
|
3052
|
+
}
|
|
3053
|
+
for (const id of modifiedPages) {
|
|
3054
|
+
console.log(` \x1B[33m~ ${id}\x1B[0m`);
|
|
3055
|
+
const localCompIds = new Set((local.pages[id].components || []).map((c) => c.id));
|
|
3056
|
+
const remoteCompIds = new Set((remote.pages[id].components || []).map((c) => c.id));
|
|
3057
|
+
for (const cid of localCompIds) {
|
|
3058
|
+
if (!remoteCompIds.has(cid)) console.log(` \x1B[32m+ component: ${cid}\x1B[0m`);
|
|
3059
|
+
}
|
|
3060
|
+
for (const cid of remoteCompIds) {
|
|
3061
|
+
if (!localCompIds.has(cid)) console.log(` \x1B[31m- component: ${cid}\x1B[0m`);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
const localEdges = (local.routing?.edges || []).map((e) => `${e.from}->${e.to}`);
|
|
3066
|
+
const remoteEdges = (remote.routing?.edges || []).map((e) => `${e.from}->${e.to}`);
|
|
3067
|
+
const localEdgeSet = new Set(localEdges);
|
|
3068
|
+
const remoteEdgeSet = new Set(remoteEdges);
|
|
3069
|
+
const addedEdges = localEdges.filter((e) => !remoteEdgeSet.has(e));
|
|
3070
|
+
const removedEdges = remoteEdges.filter((e) => !localEdgeSet.has(e));
|
|
3071
|
+
if (addedEdges.length + removedEdges.length === 0) {
|
|
3072
|
+
console.log(" Edges: no changes");
|
|
3073
|
+
} else {
|
|
3074
|
+
console.log(" Edges:");
|
|
3075
|
+
for (const e of addedEdges) console.log(` \x1B[32m+ ${e}\x1B[0m`);
|
|
3076
|
+
for (const e of removedEdges) console.log(` \x1B[31m- ${e}\x1B[0m`);
|
|
3077
|
+
}
|
|
3078
|
+
const totalChanges = addedPages.length + removedPages.length + modifiedPages.length + addedEdges.length + removedEdges.length;
|
|
3079
|
+
console.log();
|
|
3080
|
+
console.log(
|
|
3081
|
+
` Summary: ${addedPages.length} page(s) added, ${removedPages.length} removed, ${modifiedPages.length} modified | ${addedEdges.length} edge(s) added, ${removedEdges.length} removed`
|
|
3082
|
+
);
|
|
3083
|
+
if (totalChanges === 0) {
|
|
3084
|
+
console.log(" \x1B[32m\u2713 Local matches remote\x1B[0m");
|
|
3085
|
+
}
|
|
3086
|
+
console.log();
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
// src/commands/catalog-open.ts
|
|
3090
|
+
import { exec } from "child_process";
|
|
3091
|
+
import { platform } from "os";
|
|
3092
|
+
import ora8 from "ora";
|
|
3093
|
+
async function catalogOpen(slug) {
|
|
3094
|
+
const config = requireConfig();
|
|
3095
|
+
const api = new ApiClient(config);
|
|
3096
|
+
await printIdentity(api);
|
|
3097
|
+
const spinner = ora8(`Looking up catalog "${slug}"...`).start();
|
|
3098
|
+
try {
|
|
3099
|
+
const listRes = await api.get("/api/v1/catalogs");
|
|
3100
|
+
const catalogs = listRes.data || [];
|
|
3101
|
+
const catalog2 = catalogs.find((c) => c.slug === slug);
|
|
3102
|
+
if (!catalog2) {
|
|
3103
|
+
spinner.fail(`Catalog "${slug}" not found.`);
|
|
3104
|
+
process.exit(1);
|
|
3105
|
+
}
|
|
3106
|
+
let url = catalog2.url;
|
|
3107
|
+
if (!url) {
|
|
3108
|
+
try {
|
|
3109
|
+
const me = await api.get("/api/v1/me");
|
|
3110
|
+
const subdomain = me.data?.subdomain || me.data?.app_slug;
|
|
3111
|
+
if (subdomain) {
|
|
3112
|
+
url = `https://${subdomain}.catalogkit.cc/${slug}`;
|
|
3113
|
+
}
|
|
3114
|
+
} catch {
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
if (!url) {
|
|
3118
|
+
url = `https://catalogkit.cc/c/${catalog2.catalog_id}`;
|
|
3119
|
+
}
|
|
3120
|
+
spinner.succeed(`Opening: ${url}`);
|
|
3121
|
+
const os = platform();
|
|
3122
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
3123
|
+
exec(`${cmd} "${url}"`, (err) => {
|
|
3124
|
+
if (err) {
|
|
3125
|
+
console.log(`
|
|
3126
|
+
Could not open browser. Visit: ${url}
|
|
3127
|
+
`);
|
|
3128
|
+
}
|
|
3129
|
+
});
|
|
3130
|
+
} catch (err) {
|
|
3131
|
+
spinner.fail(`Failed: ${err.message}`);
|
|
3132
|
+
process.exit(1);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
1954
3136
|
// src/commands/whoami.ts
|
|
1955
3137
|
async function whoami() {
|
|
1956
3138
|
const config = getConfig();
|
|
@@ -2009,7 +3191,7 @@ async function whoami() {
|
|
|
2009
3191
|
|
|
2010
3192
|
// src/index.ts
|
|
2011
3193
|
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
2012
|
-
var { version } = JSON.parse(readFileSync5(
|
|
3194
|
+
var { version } = JSON.parse(readFileSync5(join4(__dirname, "../package.json"), "utf-8"));
|
|
2013
3195
|
var program = new Command();
|
|
2014
3196
|
program.name("catalogs").description("CLI for Catalog Kit \u2014 upload videos, push catalogs, manage assets").version(version).option("--token <token>", "Auth token (overrides CATALOG_KIT_TOKEN env var)").hook("preAction", (thisCommand) => {
|
|
2015
3197
|
const opts = thisCommand.opts();
|
|
@@ -2024,5 +3206,9 @@ var catalog = program.command("catalog").description("Catalog schema management"
|
|
|
2024
3206
|
catalog.command("push <file>").description("Create or update a catalog from a JSON or TypeScript schema file").option("--publish", "Set status to published (default: draft)").action(catalogPush);
|
|
2025
3207
|
catalog.command("list").description("List all catalogs").action(catalogList);
|
|
2026
3208
|
catalog.command("dev <file>").description("Preview a catalog locally with hot reload and local asset serving").option("--port <port>", "Port to serve on (default: 3456)").action(catalogDev);
|
|
3209
|
+
catalog.command("validate <file>").description("Validate a catalog schema (no token required)").action(catalogValidate);
|
|
3210
|
+
catalog.command("init").description("Scaffold a new catalog from a template").action(catalogInit);
|
|
3211
|
+
catalog.command("diff <file>").description("Compare local catalog schema against remote").action(catalogDiff);
|
|
3212
|
+
catalog.command("open <slug>").description("Open a published catalog in the browser").action(catalogOpen);
|
|
2027
3213
|
program.command("whoami").description("Show current authentication info").action(whoami);
|
|
2028
3214
|
program.parse();
|