@burdenoff/website-sdk 2026.1101.1 → 2026.1101.3
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/README.md +52 -11
- package/dist/analytics/index.js +19 -69
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +3 -69
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/avatar-live/index.js +1 -0
- package/dist/avatar-live/index.js.map +1 -1
- package/dist/avatar-live/index.mjs +1 -0
- package/dist/avatar-live/index.mjs.map +1 -1
- package/dist/avatar-live-view/index.js +25 -30
- package/dist/avatar-live-view/index.js.map +1 -1
- package/dist/avatar-live-view/index.mjs +20 -25
- package/dist/avatar-live-view/index.mjs.map +1 -1
- package/dist/chunk-23H4JUL4.js +41 -0
- package/dist/chunk-23H4JUL4.js.map +1 -0
- package/dist/chunk-34RX5P75.mjs +25 -0
- package/dist/chunk-34RX5P75.mjs.map +1 -0
- package/dist/chunk-3LFFY7AZ.js +184 -0
- package/dist/chunk-3LFFY7AZ.js.map +1 -0
- package/dist/chunk-3X26GE4S.js +2794 -0
- package/dist/chunk-3X26GE4S.js.map +1 -0
- package/dist/chunk-45GCA4GG.js +85 -0
- package/dist/chunk-45GCA4GG.js.map +1 -0
- package/dist/chunk-4SAGVLHN.js +876 -0
- package/dist/chunk-4SAGVLHN.js.map +1 -0
- package/dist/chunk-4U6YAII5.js +218 -0
- package/dist/chunk-4U6YAII5.js.map +1 -0
- package/dist/chunk-5GMTBOO6.js +21 -0
- package/dist/chunk-5GMTBOO6.js.map +1 -0
- package/dist/chunk-5JRJ57CL.mjs +329 -0
- package/dist/chunk-5JRJ57CL.mjs.map +1 -0
- package/dist/chunk-6C2DKIC7.js +4 -0
- package/dist/chunk-6C2DKIC7.js.map +1 -0
- package/dist/chunk-6IBFBWC5.mjs +17 -0
- package/dist/chunk-6IBFBWC5.mjs.map +1 -0
- package/dist/chunk-6KY2ZRVI.js +116 -0
- package/dist/chunk-6KY2ZRVI.js.map +1 -0
- package/dist/chunk-6VJT3SM4.mjs +182 -0
- package/dist/chunk-6VJT3SM4.mjs.map +1 -0
- package/dist/chunk-6YZUFMK2.mjs +40 -0
- package/dist/chunk-6YZUFMK2.mjs.map +1 -0
- package/dist/chunk-7A6R7CGG.js +47 -0
- package/dist/chunk-7A6R7CGG.js.map +1 -0
- package/dist/chunk-7PWL4V2R.js +4 -0
- package/dist/chunk-7PWL4V2R.js.map +1 -0
- package/dist/chunk-AA33WDYS.js +161 -0
- package/dist/chunk-AA33WDYS.js.map +1 -0
- package/dist/chunk-AHRGFXV2.js +525 -0
- package/dist/chunk-AHRGFXV2.js.map +1 -0
- package/dist/chunk-B63XLDZH.mjs +109 -0
- package/dist/chunk-B63XLDZH.mjs.map +1 -0
- package/dist/chunk-BX2USEWF.mjs +35 -0
- package/dist/chunk-BX2USEWF.mjs.map +1 -0
- package/dist/chunk-BYSZT35R.js +150 -0
- package/dist/chunk-BYSZT35R.js.map +1 -0
- package/dist/chunk-C6OJIHNO.js +12 -0
- package/dist/chunk-C6OJIHNO.js.map +1 -0
- package/dist/chunk-COOELBX7.mjs +63 -0
- package/dist/chunk-COOELBX7.mjs.map +1 -0
- package/dist/chunk-D2TW7MNC.mjs +2771 -0
- package/dist/chunk-D2TW7MNC.mjs.map +1 -0
- package/dist/chunk-D5KEZ6V6.mjs +3 -0
- package/dist/chunk-D5KEZ6V6.mjs.map +1 -0
- package/dist/chunk-E3GUGJMY.js +46 -0
- package/dist/chunk-E3GUGJMY.js.map +1 -0
- package/dist/chunk-EBZ2HGJ2.mjs +52 -0
- package/dist/chunk-EBZ2HGJ2.mjs.map +1 -0
- package/dist/chunk-EDLTUOIA.mjs +210 -0
- package/dist/chunk-EDLTUOIA.mjs.map +1 -0
- package/dist/chunk-EJQSOT4X.js +4 -0
- package/dist/chunk-EJQSOT4X.js.map +1 -0
- package/dist/chunk-EUPFS6MZ.mjs +2019 -0
- package/dist/chunk-EUPFS6MZ.mjs.map +1 -0
- package/dist/chunk-F3XWDC3B.js +77 -0
- package/dist/chunk-F3XWDC3B.js.map +1 -0
- package/dist/chunk-FFADMVIK.mjs +5127 -0
- package/dist/chunk-FFADMVIK.mjs.map +1 -0
- package/dist/chunk-FJWS7BKT.mjs +3 -0
- package/dist/chunk-FJWS7BKT.mjs.map +1 -0
- package/dist/chunk-FJZJDUOR.mjs +3 -0
- package/dist/chunk-FJZJDUOR.mjs.map +1 -0
- package/dist/chunk-GCFLCO2Z.mjs +145 -0
- package/dist/chunk-GCFLCO2Z.mjs.map +1 -0
- package/dist/chunk-GEXC4LRP.js +61 -0
- package/dist/chunk-GEXC4LRP.js.map +1 -0
- package/dist/chunk-GTRAXQGC.mjs +3 -0
- package/dist/chunk-GTRAXQGC.mjs.map +1 -0
- package/dist/chunk-IKOIRUKW.js +270 -0
- package/dist/chunk-IKOIRUKW.js.map +1 -0
- package/dist/chunk-IXTWOPRN.js +45 -0
- package/dist/chunk-IXTWOPRN.js.map +1 -0
- package/dist/chunk-JDXOXGOK.js +547 -0
- package/dist/chunk-JDXOXGOK.js.map +1 -0
- package/dist/chunk-JSLDWYV7.mjs +558 -0
- package/dist/chunk-JSLDWYV7.mjs.map +1 -0
- package/dist/chunk-JXLSTVBK.js +336 -0
- package/dist/chunk-JXLSTVBK.js.map +1 -0
- package/dist/chunk-KHYXK7FG.mjs +49 -0
- package/dist/chunk-KHYXK7FG.mjs.map +1 -0
- package/dist/chunk-KSMBOWQQ.js +2034 -0
- package/dist/chunk-KSMBOWQQ.js.map +1 -0
- package/dist/chunk-KU2PXHGB.mjs +83 -0
- package/dist/chunk-KU2PXHGB.mjs.map +1 -0
- package/dist/chunk-KZANIV3S.mjs +37 -0
- package/dist/chunk-KZANIV3S.mjs.map +1 -0
- package/dist/chunk-L4BP6YOB.js +20 -0
- package/dist/chunk-L4BP6YOB.js.map +1 -0
- package/dist/chunk-MRKA2OAF.mjs +75 -0
- package/dist/chunk-MRKA2OAF.mjs.map +1 -0
- package/dist/chunk-N5EC53MH.mjs +80 -0
- package/dist/chunk-N5EC53MH.mjs.map +1 -0
- package/dist/chunk-NZANQGDM.mjs +34 -0
- package/dist/chunk-NZANQGDM.mjs.map +1 -0
- package/dist/chunk-O4GS3XPZ.js +10 -0
- package/dist/chunk-O4GS3XPZ.js.map +1 -0
- package/dist/chunk-O76QDGRM.js +5154 -0
- package/dist/chunk-O76QDGRM.js.map +1 -0
- package/dist/chunk-OYEUNZJY.js +114 -0
- package/dist/chunk-OYEUNZJY.js.map +1 -0
- package/dist/chunk-PHXAGQNB.js +74 -0
- package/dist/chunk-PHXAGQNB.js.map +1 -0
- package/dist/chunk-PIHHPP7J.mjs +16 -0
- package/dist/chunk-PIHHPP7J.mjs.map +1 -0
- package/dist/chunk-PXRHVVOU.mjs +499 -0
- package/dist/chunk-PXRHVVOU.mjs.map +1 -0
- package/dist/chunk-QMA6U6AU.js +36 -0
- package/dist/chunk-QMA6U6AU.js.map +1 -0
- package/dist/chunk-R6J7IULF.js +44 -0
- package/dist/chunk-R6J7IULF.js.map +1 -0
- package/dist/chunk-R7G6TWIS.mjs +10 -0
- package/dist/chunk-R7G6TWIS.mjs.map +1 -0
- package/dist/chunk-RUMGFI6L.mjs +544 -0
- package/dist/chunk-RUMGFI6L.mjs.map +1 -0
- package/dist/chunk-S54JWSJF.js +565 -0
- package/dist/chunk-S54JWSJF.js.map +1 -0
- package/dist/chunk-SK5UGG6A.js +2041 -0
- package/dist/chunk-SK5UGG6A.js.map +1 -0
- package/dist/chunk-SKIYDKIS.mjs +70 -0
- package/dist/chunk-SKIYDKIS.mjs.map +1 -0
- package/dist/chunk-SZNS5IK6.mjs +322 -0
- package/dist/chunk-SZNS5IK6.mjs.map +1 -0
- package/dist/chunk-T7JIPZAC.js +66 -0
- package/dist/chunk-T7JIPZAC.js.map +1 -0
- package/dist/chunk-TGYQHO2E.mjs +864 -0
- package/dist/chunk-TGYQHO2E.mjs.map +1 -0
- package/dist/chunk-TT3MHN42.mjs +8 -0
- package/dist/chunk-TT3MHN42.mjs.map +1 -0
- package/dist/chunk-UM55U65W.mjs +268 -0
- package/dist/chunk-UM55U65W.mjs.map +1 -0
- package/dist/chunk-UQ2HALUD.js +72 -0
- package/dist/chunk-UQ2HALUD.js.map +1 -0
- package/dist/chunk-UXWT7G77.js +82 -0
- package/dist/chunk-UXWT7G77.js.map +1 -0
- package/dist/chunk-VNS5WDWO.mjs +7 -0
- package/dist/chunk-VNS5WDWO.mjs.map +1 -0
- package/dist/chunk-VRWZLGR7.js +4 -0
- package/dist/chunk-VRWZLGR7.js.map +1 -0
- package/dist/chunk-VZLDSNH4.mjs +22 -0
- package/dist/chunk-VZLDSNH4.mjs.map +1 -0
- package/dist/chunk-W6P3YETJ.js +92 -0
- package/dist/chunk-W6P3YETJ.js.map +1 -0
- package/dist/chunk-WPKFFQCR.mjs +90 -0
- package/dist/chunk-WPKFFQCR.mjs.map +1 -0
- package/dist/chunk-WTVTI5TO.mjs +22 -0
- package/dist/chunk-WTVTI5TO.mjs.map +1 -0
- package/dist/chunk-XJOBZ4RJ.js +44 -0
- package/dist/chunk-XJOBZ4RJ.js.map +1 -0
- package/dist/chunk-XP2JDNDU.js +325 -0
- package/dist/chunk-XP2JDNDU.js.map +1 -0
- package/dist/chunk-Y4V5CEWD.mjs +112 -0
- package/dist/chunk-Y4V5CEWD.mjs.map +1 -0
- package/dist/chunk-YERN2UWM.js +116 -0
- package/dist/chunk-YERN2UWM.js.map +1 -0
- package/dist/chunk-ZKJQWFAA.mjs +114 -0
- package/dist/chunk-ZKJQWFAA.mjs.map +1 -0
- package/dist/chunk-ZOON3S3Y.mjs +138 -0
- package/dist/chunk-ZOON3S3Y.mjs.map +1 -0
- package/dist/chunk-ZP6QRAYC.mjs +2024 -0
- package/dist/chunk-ZP6QRAYC.mjs.map +1 -0
- package/dist/chunk-ZSTFX7FV.js +9 -0
- package/dist/chunk-ZSTFX7FV.js.map +1 -0
- package/dist/client/index.js +23 -331
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -330
- package/dist/client/index.mjs.map +1 -1
- package/dist/comparison/index.js +28 -111
- package/dist/comparison/index.js.map +1 -1
- package/dist/comparison/index.mjs +2 -107
- package/dist/comparison/index.mjs.map +1 -1
- package/dist/components/contact.js +21 -931
- package/dist/components/contact.js.map +1 -1
- package/dist/components/contact.mjs +12 -909
- package/dist/components/contact.mjs.map +1 -1
- package/dist/components/electron-download.js +12 -124
- package/dist/components/electron-download.js.map +1 -1
- package/dist/components/electron-download.mjs +4 -106
- package/dist/components/electron-download.mjs.map +1 -1
- package/dist/components/explore-cta.js +8 -112
- package/dist/components/explore-cta.js.map +1 -1
- package/dist/components/explore-cta.mjs +3 -114
- package/dist/components/explore-cta.mjs.map +1 -1
- package/dist/components/explore.js +62 -7833
- package/dist/components/explore.js.map +1 -1
- package/dist/components/explore.mjs +13 -7800
- package/dist/components/explore.mjs.map +1 -1
- package/dist/components/launch-countdown.js +8 -83
- package/dist/components/launch-countdown.js.map +1 -1
- package/dist/components/launch-countdown.mjs +3 -85
- package/dist/components/launch-countdown.mjs.map +1 -1
- package/dist/components/newsletter.js +20 -993
- package/dist/components/newsletter.js.map +1 -1
- package/dist/components/newsletter.mjs +11 -971
- package/dist/components/newsletter.mjs.map +1 -1
- package/dist/components/partners.js +24 -1302
- package/dist/components/partners.js.map +1 -1
- package/dist/components/partners.mjs +15 -1276
- package/dist/components/partners.mjs.map +1 -1
- package/dist/components/pricing.js +49 -1249
- package/dist/components/pricing.js.map +1 -1
- package/dist/components/pricing.mjs +8 -1226
- package/dist/components/pricing.mjs.map +1 -1
- package/dist/components/problems-solved.js +8 -150
- package/dist/components/problems-solved.js.map +1 -1
- package/dist/components/problems-solved.mjs +4 -133
- package/dist/components/problems-solved.mjs.map +1 -1
- package/dist/components/resource-links.js +8 -304
- package/dist/components/resource-links.js.map +1 -1
- package/dist/components/resource-links.mjs +5 -308
- package/dist/components/resource-links.mjs.map +1 -1
- package/dist/components/social-links.js +8 -343
- package/dist/components/social-links.js.map +1 -1
- package/dist/components/social-links.mjs +5 -347
- package/dist/components/social-links.mjs.map +1 -1
- package/dist/components/unsubscribe.js +10 -627
- package/dist/components/unsubscribe.js.map +1 -1
- package/dist/components/unsubscribe.mjs +6 -611
- package/dist/components/unsubscribe.mjs.map +1 -1
- package/dist/components/waitlist.js +21 -1001
- package/dist/components/waitlist.js.map +1 -1
- package/dist/components/waitlist.mjs +12 -982
- package/dist/components/waitlist.mjs.map +1 -1
- package/dist/components/website-switcher.js +12 -175
- package/dist/components/website-switcher.js.map +1 -1
- package/dist/components/website-switcher.mjs +4 -157
- package/dist/components/website-switcher.mjs.map +1 -1
- package/dist/content/index.js +118 -3616
- package/dist/content/index.js.map +1 -1
- package/dist/content/index.mjs +12 -3567
- package/dist/content/index.mjs.map +1 -1
- package/dist/hooks/index.js +37 -2321
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +6 -2318
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4145 -20599
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3766 -20567
- package/dist/index.mjs.map +1 -1
- package/dist/product/index.js +20 -393
- package/dist/product/index.js.map +1 -1
- package/dist/product/index.mjs +5 -394
- package/dist/product/index.mjs.map +1 -1
- package/dist/seo/index.js +8 -76
- package/dist/seo/index.js.map +1 -1
- package/dist/seo/index.mjs +3 -78
- package/dist/seo/index.mjs.map +1 -1
- package/dist/store/index.js +51 -2650
- package/dist/store/index.js.map +1 -1
- package/dist/store/index.mjs +13 -2619
- package/dist/store/index.mjs.map +1 -1
- package/dist/utils/index.js +69 -134
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +5 -122
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/comparison/index.ts
|
|
2
|
+
var DEFAULT_COMPARISON_MAX_AGE_DAYS = 180;
|
|
3
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
5
|
+
function parseVerifiedAt(value) {
|
|
6
|
+
if (typeof value !== "string" || !ISO_DATE.test(value)) return null;
|
|
7
|
+
const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
8
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
9
|
+
return date.toISOString().slice(0, 10) === value ? date : null;
|
|
10
|
+
}
|
|
11
|
+
function isHttpUrl(value) {
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(value);
|
|
14
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function checkComparisonFreshness(claims, options = {}) {
|
|
20
|
+
const maxAgeDays = options.maxAgeDays ?? DEFAULT_COMPARISON_MAX_AGE_DAYS;
|
|
21
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
22
|
+
const issues = [];
|
|
23
|
+
for (const claim of claims) {
|
|
24
|
+
const sources = claim.sources ?? [];
|
|
25
|
+
if (sources.length === 0) {
|
|
26
|
+
issues.push({
|
|
27
|
+
path: claim.path,
|
|
28
|
+
problem: "missing-source",
|
|
29
|
+
detail: "no source: add { url, verifiedAt } for the vendor page this claim was checked against"
|
|
30
|
+
});
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
let newest = null;
|
|
34
|
+
for (const source of sources) {
|
|
35
|
+
if (!isHttpUrl(source.url)) {
|
|
36
|
+
issues.push({
|
|
37
|
+
path: claim.path,
|
|
38
|
+
problem: "invalid-url",
|
|
39
|
+
detail: `source URL is not an http(s) URL: ${JSON.stringify(source.url)}`
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const verified = parseVerifiedAt(source.verifiedAt);
|
|
43
|
+
if (!verified) {
|
|
44
|
+
issues.push({
|
|
45
|
+
path: claim.path,
|
|
46
|
+
problem: "invalid-date",
|
|
47
|
+
detail: `verifiedAt must be YYYY-MM-DD, got ${JSON.stringify(source.verifiedAt)}`
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (verified.getTime() > now.getTime() + DAY_MS) {
|
|
52
|
+
issues.push({
|
|
53
|
+
path: claim.path,
|
|
54
|
+
problem: "future-date",
|
|
55
|
+
detail: `verifiedAt ${source.verifiedAt} is in the future`
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!newest || verified.getTime() > newest.getTime()) newest = verified;
|
|
60
|
+
}
|
|
61
|
+
if (newest) {
|
|
62
|
+
const ageDays = Math.floor((now.getTime() - newest.getTime()) / DAY_MS);
|
|
63
|
+
if (ageDays > maxAgeDays) {
|
|
64
|
+
issues.push({
|
|
65
|
+
path: claim.path,
|
|
66
|
+
problem: "stale",
|
|
67
|
+
detail: `last verified ${newest.toISOString().slice(0, 10)} (${ageDays} days ago, limit ${maxAgeDays}): re-read the vendor page and update verifiedAt or the claim`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return issues;
|
|
73
|
+
}
|
|
74
|
+
function latestVerification(claims) {
|
|
75
|
+
let latest = null;
|
|
76
|
+
for (const claim of claims) {
|
|
77
|
+
for (const source of claim.sources ?? []) {
|
|
78
|
+
if (parseVerifiedAt(source.verifiedAt) && (!latest || source.verifiedAt > latest)) {
|
|
79
|
+
latest = source.verifiedAt;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return latest;
|
|
84
|
+
}
|
|
85
|
+
function oldestVerification(claims) {
|
|
86
|
+
let oldest = null;
|
|
87
|
+
for (const claim of claims) {
|
|
88
|
+
for (const source of claim.sources ?? []) {
|
|
89
|
+
if (parseVerifiedAt(source.verifiedAt) && (!oldest || source.verifiedAt < oldest)) {
|
|
90
|
+
oldest = source.verifiedAt;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return oldest;
|
|
95
|
+
}
|
|
96
|
+
function formatFreshnessReport(issues) {
|
|
97
|
+
if (issues.length === 0)
|
|
98
|
+
return "Comparison sources: all claims sourced and fresh.";
|
|
99
|
+
const lines = issues.map((i) => ` - [${i.problem}] ${i.path}: ${i.detail}`);
|
|
100
|
+
return [
|
|
101
|
+
`Comparison sources: ${issues.length} problem(s) found.`,
|
|
102
|
+
...lines,
|
|
103
|
+
"Re-check each claim against the vendor's current page, then update the claim and its verifiedAt."
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { DEFAULT_COMPARISON_MAX_AGE_DAYS, checkComparisonFreshness, formatFreshnessReport, latestVerification, oldestVerification, parseVerifiedAt };
|
|
108
|
+
//# sourceMappingURL=chunk-B63XLDZH.mjs.map
|
|
109
|
+
//# sourceMappingURL=chunk-B63XLDZH.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/comparison/index.ts"],"names":[],"mappings":"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","file":"chunk-B63XLDZH.mjs","sourcesContent":["/**\n * @burdenoff/website-sdk - Comparison page sources and freshness (BOFF-9971)\n *\n * Every product website with a /vs (competitor comparison) page makes claims\n * about other vendors. Those claims age fast: a vendor ships the feature we\n * said it lacked, or changes its prices. This module gives every site the same\n * contract and the same check:\n *\n * - each claim carries at least one source `{ url, verifiedAt }` (enforced by\n * the `Sourced` type at compile time), and\n * - `checkComparisonFreshness` fails when a claim has no source, a source is\n * not an http(s) URL, a date is malformed or in the future, or the newest\n * verification is older than `maxAgeDays` (180 by default).\n *\n * Sites run the check in their PR Check workflow, so a stale or unsourced\n * comparison blocks the next change until someone re-reads the vendor's page.\n *\n * @example\n * ```ts\n * import { checkComparisonFreshness, formatFreshnessReport } from '@burdenoff/website-sdk/comparison'\n *\n * const issues = checkComparisonFreshness(collectClaims(COMPETITORS))\n * if (issues.length) {\n * console.error(formatFreshnessReport(issues))\n * process.exit(1)\n * }\n * ```\n */\n\n/** Where a comparison claim was checked, and when (ISO date, YYYY-MM-DD). */\nexport interface ComparisonSource {\n /** The vendor page (or docs page) the claim was checked against. */\n url: string;\n /** The day a person last read that page and confirmed the claim. */\n verifiedAt: string;\n /** Optional short label shown with the link, e.g. \"Pricing\". */\n label?: string;\n}\n\n/** A non-empty list: a sourced claim always has at least one source. */\nexport type NonEmptySources = readonly [\n ComparisonSource,\n ...ComparisonSource[],\n];\n\n/** Mix-in for any comparison claim (table row, profile, pricing row). */\nexport interface Sourced {\n sources: NonEmptySources;\n}\n\n/** One claim to check, located by a human-readable path for the report. */\nexport interface ComparisonClaim {\n /** Where the claim lives, e.g. \"zapier.featureRows[Workflow versioning]\". */\n path: string;\n /** Its sources; missing or empty fails the check. */\n sources?: readonly ComparisonSource[] | null;\n}\n\nexport type FreshnessProblem =\n \"missing-source\" | \"invalid-url\" | \"invalid-date\" | \"future-date\" | \"stale\";\n\nexport interface FreshnessIssue {\n path: string;\n problem: FreshnessProblem;\n detail: string;\n}\n\nexport interface FreshnessOptions {\n /** Oldest acceptable verification, in days. Default 180. */\n maxAgeDays?: number;\n /** The clock to check against. Default: now. */\n now?: Date;\n}\n\n/** Default staleness threshold for comparison claims. */\nexport const DEFAULT_COMPARISON_MAX_AGE_DAYS = 180;\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\nconst ISO_DATE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/** Parse a strict YYYY-MM-DD date as UTC midnight, or null when invalid. */\nexport function parseVerifiedAt(value: string): Date | null {\n if (typeof value !== \"string\" || !ISO_DATE.test(value)) return null;\n const date = new Date(`${value}T00:00:00.000Z`);\n if (Number.isNaN(date.getTime())) return null;\n // Reject rollovers such as 2026-02-31 → 2026-03-03.\n return date.toISOString().slice(0, 10) === value ? date : null;\n}\n\nfunction isHttpUrl(value: string): boolean {\n try {\n const url = new URL(value);\n return url.protocol === \"https:\" || url.protocol === \"http:\";\n } catch {\n return false;\n }\n}\n\n/**\n * Check every claim and return the problems found (empty when all is well).\n * A claim passes on its newest valid source, so adding a fresh source to an\n * old claim is enough to clear a \"stale\" issue.\n */\nexport function checkComparisonFreshness(\n claims: readonly ComparisonClaim[],\n options: FreshnessOptions = {},\n): FreshnessIssue[] {\n const maxAgeDays = options.maxAgeDays ?? DEFAULT_COMPARISON_MAX_AGE_DAYS;\n const now = options.now ?? new Date();\n const issues: FreshnessIssue[] = [];\n\n for (const claim of claims) {\n const sources = claim.sources ?? [];\n if (sources.length === 0) {\n issues.push({\n path: claim.path,\n problem: \"missing-source\",\n detail:\n \"no source: add { url, verifiedAt } for the vendor page this claim was checked against\",\n });\n continue;\n }\n\n let newest: Date | null = null;\n for (const source of sources) {\n if (!isHttpUrl(source.url)) {\n issues.push({\n path: claim.path,\n problem: \"invalid-url\",\n detail: `source URL is not an http(s) URL: ${JSON.stringify(source.url)}`,\n });\n }\n const verified = parseVerifiedAt(source.verifiedAt);\n if (!verified) {\n issues.push({\n path: claim.path,\n problem: \"invalid-date\",\n detail: `verifiedAt must be YYYY-MM-DD, got ${JSON.stringify(source.verifiedAt)}`,\n });\n continue;\n }\n if (verified.getTime() > now.getTime() + DAY_MS) {\n issues.push({\n path: claim.path,\n problem: \"future-date\",\n detail: `verifiedAt ${source.verifiedAt} is in the future`,\n });\n continue;\n }\n if (!newest || verified.getTime() > newest.getTime()) newest = verified;\n }\n\n if (newest) {\n const ageDays = Math.floor((now.getTime() - newest.getTime()) / DAY_MS);\n if (ageDays > maxAgeDays) {\n issues.push({\n path: claim.path,\n problem: \"stale\",\n detail: `last verified ${newest.toISOString().slice(0, 10)} (${ageDays} days ago, limit ${maxAgeDays}): re-read the vendor page and update verifiedAt or the claim`,\n });\n }\n }\n }\n\n return issues;\n}\n\n/** Newest valid verification date across all claims, or null. */\nexport function latestVerification(\n claims: readonly ComparisonClaim[],\n): string | null {\n let latest: string | null = null;\n for (const claim of claims) {\n for (const source of claim.sources ?? []) {\n if (\n parseVerifiedAt(source.verifiedAt) &&\n (!latest || source.verifiedAt > latest)\n ) {\n latest = source.verifiedAt;\n }\n }\n }\n return latest;\n}\n\n/** Oldest valid verification date across all claims, or null. */\nexport function oldestVerification(\n claims: readonly ComparisonClaim[],\n): string | null {\n let oldest: string | null = null;\n for (const claim of claims) {\n for (const source of claim.sources ?? []) {\n if (\n parseVerifiedAt(source.verifiedAt) &&\n (!oldest || source.verifiedAt < oldest)\n ) {\n oldest = source.verifiedAt;\n }\n }\n }\n return oldest;\n}\n\n/** A plain-text report for CI logs, one line per issue. */\nexport function formatFreshnessReport(\n issues: readonly FreshnessIssue[],\n): string {\n if (issues.length === 0)\n return \"Comparison sources: all claims sourced and fresh.\";\n const lines = issues.map((i) => ` - [${i.problem}] ${i.path}: ${i.detail}`);\n return [\n `Comparison sources: ${issues.length} problem(s) found.`,\n ...lines,\n \"Re-check each claim against the vendor's current page, then update the claim and its verifiedAt.\",\n ].join(\"\\n\");\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/components/explore-avatar-live-config.ts
|
|
2
|
+
var AVATAR_WEB_API_PATH = "/api/web-avatar/v1";
|
|
3
|
+
var SITE = /^[a-z][a-z0-9-]{1,31}$/;
|
|
4
|
+
var VERSION = /^[A-Za-z0-9._-]{1,32}$/;
|
|
5
|
+
var SITE_KEY = /^[A-Za-z0-9_-]{1,128}$/;
|
|
6
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
7
|
+
function isAvatarSite(value) {
|
|
8
|
+
return SITE.test(value);
|
|
9
|
+
}
|
|
10
|
+
function isAvatarVersion(value) {
|
|
11
|
+
return VERSION.test(value);
|
|
12
|
+
}
|
|
13
|
+
function avatarApiBase(gatewayUrl) {
|
|
14
|
+
if (!gatewayUrl) return null;
|
|
15
|
+
let url;
|
|
16
|
+
try {
|
|
17
|
+
url = new URL(gatewayUrl);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const local = url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname.toLowerCase());
|
|
22
|
+
if (url.protocol !== "https:" && !local) return null;
|
|
23
|
+
return `${url.origin}${AVATAR_WEB_API_PATH}`;
|
|
24
|
+
}
|
|
25
|
+
function liveConfigComplete(live) {
|
|
26
|
+
return typeof live.load === "function" && typeof live.site === "string" && isAvatarSite(live.site) && typeof live.manifestVersion === "string" && isAvatarVersion(live.manifestVersion) && typeof live.turnstileSiteKey === "string" && SITE_KEY.test(live.turnstileSiteKey);
|
|
27
|
+
}
|
|
28
|
+
var REVIEWED = /* @__PURE__ */ new Set(["en"]);
|
|
29
|
+
function isAvatarLiveLocaleReviewed(locale) {
|
|
30
|
+
return REVIEWED.has(locale);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { avatarApiBase, isAvatarLiveLocaleReviewed, isAvatarSite, isAvatarVersion, liveConfigComplete };
|
|
34
|
+
//# sourceMappingURL=chunk-BX2USEWF.mjs.map
|
|
35
|
+
//# sourceMappingURL=chunk-BX2USEWF.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/explore-avatar-live-config.ts"],"names":[],"mappings":";AAwCO,IAAM,mBAAA,GAAsB,oBAAA;AAEnC,IAAM,IAAA,GAAO,wBAAA;AACb,IAAM,OAAA,GAAU,wBAAA;AAChB,IAAM,QAAA,GAAW,wBAAA;AACjB,IAAM,iCAAiB,IAAI,GAAA,CAAI,CAAC,WAAA,EAAa,WAAA,EAAa,OAAO,CAAC,CAAA;AAG3D,SAAS,aAAa,KAAA,EAAwB;AACnD,EAAA,OAAO,IAAA,CAAK,KAAK,KAAK,CAAA;AACxB;AAGO,SAAS,gBAAgB,KAAA,EAAwB;AACtD,EAAA,OAAO,OAAA,CAAQ,KAAK,KAAK,CAAA;AAC3B;AAOO,SAAS,cAAc,UAAA,EAA+C;AAC3E,EAAA,IAAI,CAAC,YAAY,OAAO,IAAA;AACxB,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAI,IAAI,UAAU,CAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,MAAM,KAAA,GACJ,IAAI,QAAA,KAAa,OAAA,IAAW,eAAe,GAAA,CAAI,GAAA,CAAI,QAAA,CAAS,WAAA,EAAa,CAAA;AAC3E,EAAA,IAAI,GAAA,CAAI,QAAA,KAAa,QAAA,IAAY,CAAC,OAAO,OAAO,IAAA;AAChD,EAAA,OAAO,CAAA,EAAG,GAAA,CAAI,MAAM,CAAA,EAAG,mBAAmB,CAAA,CAAA;AAC5C;AAGO,SAAS,mBAAmB,IAAA,EAAuC;AACxE,EAAA,OACE,OAAO,IAAA,CAAK,IAAA,KAAS,UAAA,IACrB,OAAO,IAAA,CAAK,IAAA,KAAS,QAAA,IACrB,YAAA,CAAa,IAAA,CAAK,IAAI,CAAA,IACtB,OAAO,IAAA,CAAK,eAAA,KAAoB,QAAA,IAChC,eAAA,CAAgB,IAAA,CAAK,eAAe,CAAA,IACpC,OAAO,IAAA,CAAK,gBAAA,KAAqB,QAAA,IACjC,QAAA,CAAS,IAAA,CAAK,IAAA,CAAK,gBAAgB,CAAA;AAEvC;AAMA,IAAM,QAAA,mBAAuC,IAAI,GAAA,CAAmB,CAAC,IAAI,CAAC,CAAA;AAGnE,SAAS,2BAA2B,MAAA,EAAgC;AACzE,EAAA,OAAO,QAAA,CAAS,IAAI,MAAM,CAAA;AAC5B","file":"chunk-BX2USEWF.mjs","sourcesContent":["/**\n * BOFF-9797. The small part of the live avatar that the panel itself needs, whether or not it is ever opened: what a\n * host's `avatar.live` is, whether the live avatar can run on this page at all, and where its requests go.\n *\n * Everything else (the view, its state machine, the requests, the words, the bot check) is a separate chunk that a\n * site only downloads when its visitor first opens the Avatar view (see `explore-avatar-live.tsx`). That is why this\n * file is separate and kept to a few lines: it is the only part of the live avatar in the package root for a site that\n * does not pass `avatar.live`, and the part of it that decides, without loading anything, whether the chunk is wanted.\n * Types only, plus plain functions: nothing here imports a component, a context or a string table.\n */\nimport type { ExploreLocale } from \"./explore-i18n\";\nimport type { AvatarLiveModule } from \"../avatar-live/types\";\nimport type { UseExploreChatResult } from \"../hooks/use-explore-chat\";\n\n/**\n * BOFF-9797. A host's opt-in to the live avatar on its panel's Avatar tab. Every field is required; a config that is\n * incomplete leaves the tab as it was (and a warning in the console).\n */\nexport interface ExplorePanelAvatarLive {\n /** The site, as the live avatar service knows it: `healthybowl`. */\n site: string;\n /**\n * Loads the live avatar's room module: `() => import('@burdenoff/website-sdk/avatar-live')`. Called only after the\n * visitor presses Start on the consent card, never on page load or on opening the tab. That entry needs the\n * package's optional peer dependency for media rooms (see the README).\n */\n load: () => Promise<AvatarLiveModule>;\n /** The site's Cloudflare Turnstile site key, for the bot check that runs when the visitor presses Start. */\n turnstileSiteKey: string;\n /** The version of the suggested-question list the panel shows (its `examplePrompts`), such as `hb-en-1`. */\n manifestVersion: string;\n}\n\n/** What the live avatar reads of the panel's conversation: the transcript, whether it is read-only, and the token. */\nexport type AvatarConversation = Pick<\n UseExploreChatResult,\n \"messages\" | \"isSharedView\" | \"getConversationToken\"\n>;\n\n/** The web branch's routes, under the gateway's origin. */\nexport const AVATAR_WEB_API_PATH = \"/api/web-avatar/v1\";\n\nconst SITE = /^[a-z][a-z0-9-]{1,31}$/;\nconst VERSION = /^[A-Za-z0-9._-]{1,32}$/;\nconst SITE_KEY = /^[A-Za-z0-9_-]{1,128}$/;\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n\n/** A site slug the contract accepts (`healthybowl`). */\nexport function isAvatarSite(value: string): boolean {\n return SITE.test(value);\n}\n\n/** A consent or manifest version the contract accepts (`hb-en-1`). */\nexport function isAvatarVersion(value: string): boolean {\n return VERSION.test(value);\n}\n\n/**\n * The web branch's base URL on the SDK's configured gateway: the gateway URL's origin, plus\n * {@link AVATAR_WEB_API_PATH}. Null for anything that is not an `https:` URL (or `http:` on this machine, for local\n * development).\n */\nexport function avatarApiBase(gatewayUrl: string | undefined): string | null {\n if (!gatewayUrl) return null;\n let url: URL;\n try {\n url = new URL(gatewayUrl);\n } catch {\n return null;\n }\n const local =\n url.protocol === \"http:\" && LOOPBACK_HOSTS.has(url.hostname.toLowerCase());\n if (url.protocol !== \"https:\" && !local) return null;\n return `${url.origin}${AVATAR_WEB_API_PATH}`;\n}\n\n/** Whether a host's `avatar.live` has everything the live avatar needs, in the shapes the contract accepts. */\nexport function liveConfigComplete(live: ExplorePanelAvatarLive): boolean {\n return (\n typeof live.load === \"function\" &&\n typeof live.site === \"string\" &&\n isAvatarSite(live.site) &&\n typeof live.manifestVersion === \"string\" &&\n isAvatarVersion(live.manifestVersion) &&\n typeof live.turnstileSiteKey === \"string\" &&\n SITE_KEY.test(live.turnstileSiteKey)\n );\n}\n\n/**\n * The locales whose live-avatar strings have been reviewed. The live avatar runs only in these; see\n * `explore-avatar-i18n.ts`. English only, until a named reviewer signs off on another.\n */\nconst REVIEWED: ReadonlySet<ExploreLocale> = new Set<ExploreLocale>([\"en\"]);\n\n/** Whether the live avatar may run in `locale`: its strings have been reviewed. */\nexport function isAvatarLiveLocaleReviewed(locale: ExploreLocale): boolean {\n return REVIEWED.has(locale);\n}\n"]}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkE3GUGJMY_js = require('./chunk-E3GUGJMY.js');
|
|
4
|
+
var chunkJXLSTVBK_js = require('./chunk-JXLSTVBK.js');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
var PRODUCT_FIELDS_BASE = `
|
|
9
|
+
id
|
|
10
|
+
name
|
|
11
|
+
slug
|
|
12
|
+
description
|
|
13
|
+
status
|
|
14
|
+
logoUrl
|
|
15
|
+
website
|
|
16
|
+
appUrl
|
|
17
|
+
docsUrl
|
|
18
|
+
contactEmail
|
|
19
|
+
supportEmail
|
|
20
|
+
contactPhone
|
|
21
|
+
contactAddress
|
|
22
|
+
seoTitle
|
|
23
|
+
seoDescription
|
|
24
|
+
seoKeywords
|
|
25
|
+
ogImage
|
|
26
|
+
socialLinks
|
|
27
|
+
recaptchaSiteKey
|
|
28
|
+
rybbitSiteId
|
|
29
|
+
notificationEmail
|
|
30
|
+
metadata
|
|
31
|
+
`;
|
|
32
|
+
var PUBLIC_PRODUCT_QUERY = `
|
|
33
|
+
query PublicProduct($slug: String!) {
|
|
34
|
+
publicProduct(slug: $slug) {
|
|
35
|
+
${PRODUCT_FIELDS_BASE}
|
|
36
|
+
resourceLinks
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
var PUBLIC_PRODUCT_QUERY_FALLBACK = `
|
|
41
|
+
query PublicProductFallback($slug: String!) {
|
|
42
|
+
publicProduct(slug: $slug) {
|
|
43
|
+
${PRODUCT_FIELDS_BASE}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
var ProductContext = react.createContext({
|
|
48
|
+
product: null,
|
|
49
|
+
loading: true,
|
|
50
|
+
error: null,
|
|
51
|
+
hasProvider: false,
|
|
52
|
+
refetch: () => {
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
async function queryPublicProduct(client, slug) {
|
|
56
|
+
const result = await client.query(
|
|
57
|
+
PUBLIC_PRODUCT_QUERY,
|
|
58
|
+
{ slug }
|
|
59
|
+
);
|
|
60
|
+
const schemaMissingResourceLinks = result.errors?.some(
|
|
61
|
+
(e) => /Cannot query field ["']?resourceLinks["']?/i.test(e?.message ?? "")
|
|
62
|
+
);
|
|
63
|
+
if (!schemaMissingResourceLinks) return result;
|
|
64
|
+
return client.query(
|
|
65
|
+
PUBLIC_PRODUCT_QUERY_FALLBACK,
|
|
66
|
+
{ slug }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
async function fetchPublicProduct(client, slug) {
|
|
70
|
+
for (const candidate of chunkE3GUGJMY_js.productSlugVariants(slug)) {
|
|
71
|
+
const result = await queryPublicProduct(client, candidate);
|
|
72
|
+
if (result.errors?.length) {
|
|
73
|
+
return {
|
|
74
|
+
product: null,
|
|
75
|
+
error: result.errors[0]?.message ?? "Unknown error"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (result.data?.publicProduct) {
|
|
79
|
+
return { product: result.data.publicProduct, error: null };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { product: null, error: null };
|
|
83
|
+
}
|
|
84
|
+
function ProductProvider({ children, fallback }) {
|
|
85
|
+
const client = chunkJXLSTVBK_js.useWebSDK();
|
|
86
|
+
const config = chunkJXLSTVBK_js.useWebSDKConfig();
|
|
87
|
+
const [product, setProduct] = react.useState(
|
|
88
|
+
fallback ? fallback : null
|
|
89
|
+
);
|
|
90
|
+
const [loading, setLoading] = react.useState(true);
|
|
91
|
+
const [error, setError] = react.useState(null);
|
|
92
|
+
const fetchProduct = react.useCallback(async () => {
|
|
93
|
+
if (!config.productSlug) {
|
|
94
|
+
setLoading(false);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
setLoading(true);
|
|
98
|
+
setError(null);
|
|
99
|
+
try {
|
|
100
|
+
const outcome = await fetchPublicProduct(client, config.productSlug);
|
|
101
|
+
if (outcome.error) {
|
|
102
|
+
setError(outcome.error);
|
|
103
|
+
} else if (outcome.product) {
|
|
104
|
+
setProduct(outcome.product);
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
setError(err instanceof Error ? err.message : "Failed to fetch product");
|
|
108
|
+
} finally {
|
|
109
|
+
setLoading(false);
|
|
110
|
+
}
|
|
111
|
+
}, [client, config.productSlug]);
|
|
112
|
+
react.useLayoutEffect(() => {
|
|
113
|
+
fetchProduct();
|
|
114
|
+
}, [fetchProduct]);
|
|
115
|
+
const value = react.useMemo(
|
|
116
|
+
() => ({
|
|
117
|
+
product,
|
|
118
|
+
loading,
|
|
119
|
+
error,
|
|
120
|
+
hasProvider: true,
|
|
121
|
+
refetch: fetchProduct
|
|
122
|
+
}),
|
|
123
|
+
[product, loading, error, fetchProduct]
|
|
124
|
+
);
|
|
125
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ProductContext.Provider, { value, children });
|
|
126
|
+
}
|
|
127
|
+
function useProduct() {
|
|
128
|
+
return react.useContext(ProductContext);
|
|
129
|
+
}
|
|
130
|
+
function useProductConfig() {
|
|
131
|
+
const { product } = useProduct();
|
|
132
|
+
const config = chunkJXLSTVBK_js.useWebSDKConfig();
|
|
133
|
+
return {
|
|
134
|
+
// Prefer product-level config, fall back to SDK config
|
|
135
|
+
recaptchaSiteKey: product?.recaptchaSiteKey ?? config.recaptchaSiteKey ?? null,
|
|
136
|
+
rybbitSiteId: product?.rybbitSiteId ?? config.rybbitSiteId ?? null,
|
|
137
|
+
contactEmail: product?.contactEmail ?? null,
|
|
138
|
+
supportEmail: product?.supportEmail ?? null,
|
|
139
|
+
notificationEmail: product?.notificationEmail ?? product?.contactEmail ?? null,
|
|
140
|
+
productName: product?.name ?? null,
|
|
141
|
+
logoUrl: product?.logoUrl ?? null
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
exports.ProductProvider = ProductProvider;
|
|
146
|
+
exports.fetchPublicProduct = fetchPublicProduct;
|
|
147
|
+
exports.useProduct = useProduct;
|
|
148
|
+
exports.useProductConfig = useProductConfig;
|
|
149
|
+
//# sourceMappingURL=chunk-BYSZT35R.js.map
|
|
150
|
+
//# sourceMappingURL=chunk-BYSZT35R.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/product/product-provider.tsx"],"names":["createContext","productSlugVariants","useWebSDK","useWebSDKConfig","useState","useCallback","useLayoutEffect","useMemo","jsx","useContext"],"mappings":"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","file":"chunk-BYSZT35R.js","sourcesContent":["/**\n * Product Provider\n *\n * React context and hooks for accessing product metadata from the global tenant service.\n * Fetches product info (name, logo, SEO, contact, reCAPTCHA key, etc.) on mount\n * and provides it to all child components.\n */\n\"use client\";\n\nimport {\n createContext,\n useContext,\n useState,\n useLayoutEffect,\n useCallback,\n useMemo,\n} from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useWebSDK, useWebSDKConfig } from \"../client/provider\";\nimport { productSlugVariants } from \"../utils/product-slug\";\n\nimport type { WebSDKClient } from \"../client/graphql-client\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ProductData {\n id: string;\n name: string;\n slug: string;\n description?: string | null;\n status: string;\n logoUrl?: string | null;\n website?: string | null;\n appUrl?: string | null;\n docsUrl?: string | null;\n contactEmail?: string | null;\n supportEmail?: string | null;\n contactPhone?: string | null;\n contactAddress?: string | null;\n seoTitle?: string | null;\n seoDescription?: string | null;\n seoKeywords?: string | null;\n ogImage?: string | null;\n socialLinks?: Record<string, string> | null;\n /**\n * Resource links for the website footer \"Resources\" column\n * (JSON: { docs, community, careers, team, companies, ... }).\n * Same shape as socialLinks. Free-form keys; the ResourceLinks\n * component renders known keys and silently ignores the rest.\n * Backed by Product.resourceLinks on the backend (global-tenant-svc).\n */\n resourceLinks?: Record<string, string> | null;\n recaptchaSiteKey?: string | null;\n rybbitSiteId?: string | null;\n notificationEmail?: string | null;\n metadata?: Record<string, unknown> | null;\n}\n\nexport interface ProductProviderProps {\n /** Child components */\n children: ReactNode;\n /** Optional fallback product data (used while loading) */\n fallback?: Partial<ProductData>;\n}\n\n// ============================================================================\n// GraphQL Query\n// ============================================================================\n\n/**\n * Body of fields requested for every product fetch — shared by the primary\n * query and the legacy-backend fallback below. Kept as a string fragment\n * (not a real GraphQL `fragment` block) so we can append/strip\n * `resourceLinks` without touching anything else.\n */\nconst PRODUCT_FIELDS_BASE = `\n id\n name\n slug\n description\n status\n logoUrl\n website\n appUrl\n docsUrl\n contactEmail\n supportEmail\n contactPhone\n contactAddress\n seoTitle\n seoDescription\n seoKeywords\n ogImage\n socialLinks\n recaptchaSiteKey\n rybbitSiteId\n notificationEmail\n metadata\n`;\n\n/**\n * Primary query — includes the recently-added `resourceLinks` field.\n * Backends that have run the corresponding `global-tenant-svc` migration\n * (PR #64) expose this field; older ones don't, in which case\n * GraphQL validation rejects the whole query with\n * `Cannot query field \"resourceLinks\" on type \"PublicProduct\"`. The fetch\n * helper detects that specific error and transparently retries the\n * fallback below, so the SDK works on both pre- and post-migration\n * deployments without consumer-side gating.\n */\nconst PUBLIC_PRODUCT_QUERY = `\n query PublicProduct($slug: String!) {\n publicProduct(slug: $slug) {\n ${PRODUCT_FIELDS_BASE}\n resourceLinks\n }\n }\n`;\n\n/**\n * Fallback query for older backend deployments. Identical to the primary\n * minus `resourceLinks`. Consumers see `product.resourceLinks === undefined`\n * and any consumer (e.g. `<ResourceLinks/>`) falls through to prop / default\n * resolution paths.\n */\nconst PUBLIC_PRODUCT_QUERY_FALLBACK = `\n query PublicProductFallback($slug: String!) {\n publicProduct(slug: $slug) {\n ${PRODUCT_FIELDS_BASE}\n }\n }\n`;\n\n// ============================================================================\n// Context\n// ============================================================================\n\ninterface ProductContextValue {\n product: ProductData | null;\n loading: boolean;\n error: string | null;\n hasProvider: boolean;\n refetch: () => void;\n}\n\nconst ProductContext = createContext<ProductContextValue>({\n product: null,\n loading: true,\n error: null,\n hasProvider: false,\n refetch: () => {},\n});\n\n// ============================================================================\n// Provider\n// ============================================================================\n\n/** Result of {@link fetchPublicProduct}. */\nexport interface PublicProductFetchResult {\n product: ProductData | null;\n error: string | null;\n}\n\nasync function queryPublicProduct(\n client: Pick<WebSDKClient, \"query\">,\n slug: string,\n) {\n const result = await client.query<{ publicProduct: ProductData | null }>(\n PUBLIC_PRODUCT_QUERY,\n { slug },\n );\n\n // Backwards-compat fallback: if the backend's `publicProduct` schema\n // doesn't yet expose `resourceLinks` (deployed before\n // `global-tenant-svc` PR #64), the primary query fails validation\n // with a \"Cannot query field …\" error and `result.data` is null.\n // Retry the fallback query that omits `resourceLinks`; consumers\n // gracefully see `product.resourceLinks === undefined` and fall\n // through their own resolution paths.\n const schemaMissingResourceLinks = result.errors?.some((e) =>\n /Cannot query field [\"']?resourceLinks[\"']?/i.test(e?.message ?? \"\"),\n );\n if (!schemaMissingResourceLinks) return result;\n return client.query<{ publicProduct: ProductData | null }>(\n PUBLIC_PRODUCT_QUERY_FALLBACK,\n { slug },\n );\n}\n\n/**\n * Fetch a product's public metadata by slug.\n *\n * A renamed product (`botlit` → `botmanor`) is looked up under the configured\n * slug first, then under its twin, so a site keeps resolving its product on\n * both sides of the tenant-service slug migration. The first error stops the\n * lookup; \"not found\" under every spelling returns `{ product: null, error: null }`.\n */\nexport async function fetchPublicProduct(\n client: Pick<WebSDKClient, \"query\">,\n slug: string,\n): Promise<PublicProductFetchResult> {\n for (const candidate of productSlugVariants(slug)) {\n const result = await queryPublicProduct(client, candidate);\n if (result.errors?.length) {\n return {\n product: null,\n error: result.errors[0]?.message ?? \"Unknown error\",\n };\n }\n if (result.data?.publicProduct) {\n return { product: result.data.publicProduct, error: null };\n }\n }\n return { product: null, error: null };\n}\n\n/**\n * ProductProvider fetches and provides product metadata to child components.\n * Uses the product slug from WebSDKConfig to fetch from the public API.\n *\n * @example\n * ```tsx\n * <WebSDKProvider config={sdkConfig}>\n * <ProductProvider>\n * <App />\n * </ProductProvider>\n * </WebSDKProvider>\n * ```\n */\nexport function ProductProvider({ children, fallback }: ProductProviderProps) {\n const client = useWebSDK();\n const config = useWebSDKConfig();\n const [product, setProduct] = useState<ProductData | null>(\n fallback ? (fallback as ProductData) : null,\n );\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const fetchProduct = useCallback(async () => {\n if (!config.productSlug) {\n setLoading(false);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const outcome = await fetchPublicProduct(client, config.productSlug);\n if (outcome.error) {\n setError(outcome.error);\n } else if (outcome.product) {\n setProduct(outcome.product);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to fetch product\");\n } finally {\n setLoading(false);\n }\n }, [client, config.productSlug]);\n\n // Use useLayoutEffect for earliest possible fetch after commit (before paint).\n // Runs when slug or client changes. Client-side query cache + dedup ensures\n // no duplicate network requests even if React StrictMode fires this twice.\n useLayoutEffect(() => {\n fetchProduct();\n }, [fetchProduct]);\n\n const value = useMemo(\n () => ({\n product,\n loading,\n error,\n hasProvider: true,\n refetch: fetchProduct,\n }),\n [product, loading, error, fetchProduct],\n );\n\n return (\n <ProductContext.Provider value={value}>{children}</ProductContext.Provider>\n );\n}\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\n/**\n * Hook to access the current product data.\n *\n * @example\n * ```tsx\n * const { product, loading } = useProduct();\n * if (loading) return <Loading />;\n * return <h1>{product?.name}</h1>;\n * ```\n */\nexport function useProduct(): ProductContextValue {\n return useContext(ProductContext);\n}\n\n/**\n * Hook to get product-specific configuration.\n * Merges SDK config with fetched product data.\n * Useful for getting reCAPTCHA keys, analytics IDs, etc.\n *\n * @example\n * ```tsx\n * const { recaptchaSiteKey, rybbitSiteId } = useProductConfig();\n * ```\n */\nexport function useProductConfig(): {\n recaptchaSiteKey: string | null;\n rybbitSiteId: string | null;\n contactEmail: string | null;\n supportEmail: string | null;\n notificationEmail: string | null;\n productName: string | null;\n logoUrl: string | null;\n} {\n const { product } = useProduct();\n const config = useWebSDKConfig();\n\n return {\n // Prefer product-level config, fall back to SDK config\n recaptchaSiteKey:\n product?.recaptchaSiteKey ?? config.recaptchaSiteKey ?? null,\n rybbitSiteId: product?.rybbitSiteId ?? config.rybbitSiteId ?? null,\n contactEmail: product?.contactEmail ?? null,\n supportEmail: product?.supportEmail ?? null,\n notificationEmail:\n product?.notificationEmail ?? product?.contactEmail ?? null,\n productName: product?.name ?? null,\n logoUrl: product?.logoUrl ?? null,\n };\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var clsx = require('clsx');
|
|
4
|
+
var tailwindMerge = require('tailwind-merge');
|
|
5
|
+
|
|
6
|
+
function cn(...inputs) {
|
|
7
|
+
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
exports.cn = cn;
|
|
11
|
+
//# sourceMappingURL=chunk-C6OJIHNO.js.map
|
|
12
|
+
//# sourceMappingURL=chunk-C6OJIHNO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/cn.ts"],"names":["twMerge","clsx"],"mappings":";;;;;AAmBO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,OAAOA,qBAAA,CAAQC,SAAA,CAAK,MAAM,CAAC,CAAA;AAC7B","file":"chunk-C6OJIHNO.js","sourcesContent":["/**\n * Utility function to merge Tailwind CSS classes\n * Combines clsx and tailwind-merge for optimal class handling\n */\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge multiple class names intelligently\n * - Handles conditional classes via clsx\n * - Deduplicates and resolves Tailwind conflicts via tailwind-merge\n *\n * @example\n * ```ts\n * cn('px-4 py-2', 'bg-blue-500', { 'text-white': isActive })\n * // => 'px-4 py-2 bg-blue-500 text-white' (if isActive is true)\n * ```\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Button } from './chunk-KHYXK7FG.mjs';
|
|
2
|
+
import { cn } from './chunk-R7G6TWIS.mjs';
|
|
3
|
+
import { Download } from 'lucide-react';
|
|
4
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
function detectElectronPlatform(userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "") {
|
|
7
|
+
const ua = userAgent.toLowerCase();
|
|
8
|
+
if (ua.includes("win")) return "win";
|
|
9
|
+
if (ua.includes("mac")) return "mac";
|
|
10
|
+
if (ua.includes("linux")) return "linux";
|
|
11
|
+
return "unknown";
|
|
12
|
+
}
|
|
13
|
+
function buildDownloadUrl(base, artifactName, platform, platformMap) {
|
|
14
|
+
const normalizedBase = base.replace(/\/$/, "");
|
|
15
|
+
const platformKey = platformMap[platform] ?? platform;
|
|
16
|
+
const fileName = artifactName.includes("{platform}") ? artifactName.replace(/\{platform\}/g, platformKey) : artifactName;
|
|
17
|
+
return `${normalizedBase}/${fileName}`;
|
|
18
|
+
}
|
|
19
|
+
function ElectronDownloadLink({
|
|
20
|
+
downloadBaseUrl,
|
|
21
|
+
artifactName,
|
|
22
|
+
hidden = true,
|
|
23
|
+
label = "Download desktop app",
|
|
24
|
+
downloadLabel = label,
|
|
25
|
+
platformMap = {},
|
|
26
|
+
className,
|
|
27
|
+
variant = "default",
|
|
28
|
+
size = "default"
|
|
29
|
+
}) {
|
|
30
|
+
if (hidden) return null;
|
|
31
|
+
const platform = detectElectronPlatform();
|
|
32
|
+
const href = buildDownloadUrl(
|
|
33
|
+
downloadBaseUrl,
|
|
34
|
+
artifactName,
|
|
35
|
+
platform,
|
|
36
|
+
platformMap
|
|
37
|
+
);
|
|
38
|
+
return /* @__PURE__ */ jsx(
|
|
39
|
+
Button,
|
|
40
|
+
{
|
|
41
|
+
variant,
|
|
42
|
+
size,
|
|
43
|
+
className: cn("inline-flex items-center gap-2", className),
|
|
44
|
+
asChild: true,
|
|
45
|
+
children: /* @__PURE__ */ jsxs(
|
|
46
|
+
"a",
|
|
47
|
+
{
|
|
48
|
+
href,
|
|
49
|
+
download: true,
|
|
50
|
+
"aria-label": `${downloadLabel}${platform !== "unknown" ? ` for ${platform}` : ""}`,
|
|
51
|
+
children: [
|
|
52
|
+
/* @__PURE__ */ jsx(Download, { className: "h-4 w-4", "aria-hidden": "true" }),
|
|
53
|
+
label
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { ElectronDownloadLink, detectElectronPlatform };
|
|
62
|
+
//# sourceMappingURL=chunk-COOELBX7.mjs.map
|
|
63
|
+
//# sourceMappingURL=chunk-COOELBX7.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/electron-download.tsx"],"names":[],"mappings":";;;;;AAgEO,SAAS,uBACd,SAAA,GAAoB,OAAO,cAAc,WAAA,GACrC,SAAA,CAAU,YACV,EAAA,EACc;AAClB,EAAA,MAAM,EAAA,GAAK,UAAU,WAAA,EAAY;AAEjC,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,KAAK,CAAA,EAAG,OAAO,KAAA;AAC/B,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,KAAK,CAAA,EAAG,OAAO,KAAA;AAC/B,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,OAAO,CAAA,EAAG,OAAO,OAAA;AAEjC,EAAA,OAAO,SAAA;AACT;AAEA,SAAS,gBAAA,CACP,IAAA,EACA,YAAA,EACA,QAAA,EACA,WAAA,EACQ;AACR,EAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAC7C,EAAA,MAAM,WAAA,GAAc,WAAA,CAAY,QAAQ,CAAA,IAAK,QAAA;AAC7C,EAAA,MAAM,QAAA,GAAW,aAAa,QAAA,CAAS,YAAY,IAC/C,YAAA,CAAa,OAAA,CAAQ,eAAA,EAAiB,WAAW,CAAA,GACjD,YAAA;AACJ,EAAA,OAAO,CAAA,EAAG,cAAc,CAAA,CAAA,EAAI,QAAQ,CAAA,CAAA;AACtC;AAMO,SAAS,oBAAA,CAAqB;AAAA,EACnC,eAAA;AAAA,EACA,YAAA;AAAA,EACA,MAAA,GAAS,IAAA;AAAA,EACT,KAAA,GAAQ,sBAAA;AAAA,EACR,aAAA,GAAgB,KAAA;AAAA,EAChB,cAAc,EAAC;AAAA,EACf,SAAA;AAAA,EACA,OAAA,GAAU,SAAA;AAAA,EACV,IAAA,GAAO;AACT,CAAA,EAA8B;AAC5B,EAAA,IAAI,QAAQ,OAAO,IAAA;AAEnB,EAAA,MAAM,WAAW,sBAAA,EAAuB;AACxC,EAAA,MAAM,IAAA,GAAO,gBAAA;AAAA,IACX,eAAA;AAAA,IACA,YAAA;AAAA,IACA,QAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,uBACE,GAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACC,OAAA;AAAA,MACA,IAAA;AAAA,MACA,SAAA,EAAW,EAAA,CAAG,gCAAA,EAAkC,SAAS,CAAA;AAAA,MACzD,OAAA,EAAO,IAAA;AAAA,MAEP,QAAA,kBAAA,IAAA;AAAA,QAAC,GAAA;AAAA,QAAA;AAAA,UACC,IAAA;AAAA,UACA,QAAA,EAAQ,IAAA;AAAA,UACR,YAAA,EAAY,GAAG,aAAa,CAAA,EAAG,aAAa,SAAA,GAAY,CAAA,KAAA,EAAQ,QAAQ,CAAA,CAAA,GAAK,EAAE,CAAA,CAAA;AAAA,UAE/E,QAAA,EAAA;AAAA,4BAAA,GAAA,CAAC,QAAA,EAAA,EAAS,SAAA,EAAU,SAAA,EAAU,aAAA,EAAY,MAAA,EAAO,CAAA;AAAA,YAChD;AAAA;AAAA;AAAA;AACH;AAAA,GACF;AAEJ","file":"chunk-COOELBX7.mjs","sourcesContent":["\"use client\";\n\n/**\n * ElectronDownloadLink Component\n *\n * A download link that auto-detects the visitor's operating system and points\n * to the matching Electron artifact. Hidden by default via the `hidden` prop so\n * it can be staged in pages and revealed once a desktop build is ready.\n *\n * The `artifactName` prop may contain a `{platform}` token which is replaced\n * with the detected platform key (`win`, `mac`, or `linux`).\n *\n * @example\n * ```tsx\n * import { ElectronDownloadLink } from '@burdenoff/website-sdk'\n *\n * <ElectronDownloadLink\n * downloadBaseUrl=\"https://releases.example.com/desktop\"\n * artifactName=\"MyApp-{platform}.zip\"\n * />\n * ```\n */\n\nimport { Download } from \"lucide-react\";\n\nimport { Button } from \"../ui/button\";\nimport { cn } from \"../utils/cn\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type DetectedPlatform = \"win\" | \"mac\" | \"linux\" | \"unknown\";\n\nexport interface ElectronDownloadLinkProps {\n /** Base URL where artifacts are hosted (with or without trailing slash). */\n downloadBaseUrl: string;\n /** Artifact file name; may include a `{platform}` token. */\n artifactName: string;\n /** When true, the element is not rendered. Defaults to true (hidden). */\n hidden?: boolean;\n /** Link/button text. */\n label?: string;\n /** Accessible label for the download action. */\n downloadLabel?: string;\n /** Optional mapping from detected platform to artifact platform key. */\n platformMap?: Partial<Record<DetectedPlatform, string>>;\n /** Additional CSS class for the link. */\n className?: string;\n /** Variant for the rendered button. */\n variant?: \"default\" | \"outline\" | \"ghost\" | \"link\";\n /** Size for the rendered button. */\n size?: \"default\" | \"sm\" | \"lg\" | \"icon\";\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Detect the visitor's desktop operating system from the user agent string.\n * Returns `win`, `mac`, `linux`, or `unknown`. This is intended for choosing\n * a download artifact; it intentionally does not distinguish mobile OSes.\n */\nexport function detectElectronPlatform(\n userAgent: string = typeof navigator !== \"undefined\"\n ? navigator.userAgent\n : \"\",\n): DetectedPlatform {\n const ua = userAgent.toLowerCase();\n\n if (ua.includes(\"win\")) return \"win\";\n if (ua.includes(\"mac\")) return \"mac\";\n if (ua.includes(\"linux\")) return \"linux\";\n\n return \"unknown\";\n}\n\nfunction buildDownloadUrl(\n base: string,\n artifactName: string,\n platform: DetectedPlatform,\n platformMap: Partial<Record<DetectedPlatform, string>>,\n): string {\n const normalizedBase = base.replace(/\\/$/, \"\");\n const platformKey = platformMap[platform] ?? platform;\n const fileName = artifactName.includes(\"{platform}\")\n ? artifactName.replace(/\\{platform\\}/g, platformKey)\n : artifactName;\n return `${normalizedBase}/${fileName}`;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function ElectronDownloadLink({\n downloadBaseUrl,\n artifactName,\n hidden = true,\n label = \"Download desktop app\",\n downloadLabel = label,\n platformMap = {},\n className,\n variant = \"default\",\n size = \"default\",\n}: ElectronDownloadLinkProps) {\n if (hidden) return null;\n\n const platform = detectElectronPlatform();\n const href = buildDownloadUrl(\n downloadBaseUrl,\n artifactName,\n platform,\n platformMap,\n );\n\n return (\n <Button\n variant={variant}\n size={size}\n className={cn(\"inline-flex items-center gap-2\", className)}\n asChild\n >\n <a\n href={href}\n download\n aria-label={`${downloadLabel}${platform !== \"unknown\" ? ` for ${platform}` : \"\"}`}\n >\n <Download className=\"h-4 w-4\" aria-hidden=\"true\" />\n {label}\n </a>\n </Button>\n );\n}\n"]}
|