@businessdash/sdk 0.9.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +899 -0
- package/dist/adapters-revalidate.d.ts +44 -0
- package/dist/adapters-revalidate.d.ts.map +1 -0
- package/dist/adapters-revalidate.js +68 -0
- package/dist/adapters-revalidate.js.map +1 -0
- package/dist/alpha.d.ts +21 -0
- package/dist/alpha.d.ts.map +1 -0
- package/dist/alpha.js +21 -0
- package/dist/alpha.js.map +1 -0
- package/dist/analytics-core.d.ts +34 -0
- package/dist/analytics-core.d.ts.map +1 -0
- package/dist/analytics-core.js +138 -0
- package/dist/analytics-core.js.map +1 -0
- package/dist/analytics-server.d.ts +54 -0
- package/dist/analytics-server.d.ts.map +1 -0
- package/dist/analytics-server.js +78 -0
- package/dist/analytics-server.js.map +1 -0
- package/dist/attribution-logo.d.ts +2 -0
- package/dist/attribution-logo.d.ts.map +1 -0
- package/dist/attribution-logo.js +5 -0
- package/dist/attribution-logo.js.map +1 -0
- package/dist/auth-handler.d.ts +92 -0
- package/dist/auth-handler.d.ts.map +1 -0
- package/dist/auth-handler.js +277 -0
- package/dist/auth-handler.js.map +1 -0
- package/dist/biab-forms.css +484 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1824 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +1681 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2845 -0
- package/dist/client.js.map +1 -0
- package/dist/collections.d.ts +13 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +13 -0
- package/dist/collections.js.map +1 -0
- package/dist/contracts.d.ts +14609 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +2428 -0
- package/dist/contracts.js.map +1 -0
- package/dist/data-model-schema.d.ts +962 -0
- package/dist/data-model-schema.d.ts.map +1 -0
- package/dist/data-model-schema.js +2295 -0
- package/dist/data-model-schema.js.map +1 -0
- package/dist/distribution.d.ts +38 -0
- package/dist/distribution.d.ts.map +1 -0
- package/dist/distribution.js +61 -0
- package/dist/distribution.js.map +1 -0
- package/dist/element.d.ts +121 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +276 -0
- package/dist/element.js.map +1 -0
- package/dist/enums.d.ts +25 -0
- package/dist/enums.d.ts.map +1 -0
- package/dist/enums.js +38 -0
- package/dist/enums.js.map +1 -0
- package/dist/errors.d.ts +195 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +188 -0
- package/dist/errors.js.map +1 -0
- package/dist/forms-core/autocomplete.d.ts +37 -0
- package/dist/forms-core/autocomplete.d.ts.map +1 -0
- package/dist/forms-core/autocomplete.js +82 -0
- package/dist/forms-core/autocomplete.js.map +1 -0
- package/dist/forms-core/business-function.d.ts +61 -0
- package/dist/forms-core/business-function.d.ts.map +1 -0
- package/dist/forms-core/business-function.js +149 -0
- package/dist/forms-core/business-function.js.map +1 -0
- package/dist/forms-core/calculation.d.ts +72 -0
- package/dist/forms-core/calculation.d.ts.map +1 -0
- package/dist/forms-core/calculation.js +103 -0
- package/dist/forms-core/calculation.js.map +1 -0
- package/dist/forms-core/controller.d.ts +249 -0
- package/dist/forms-core/controller.d.ts.map +1 -0
- package/dist/forms-core/controller.js +590 -0
- package/dist/forms-core/controller.js.map +1 -0
- package/dist/forms-core/elements.d.ts +44 -0
- package/dist/forms-core/elements.d.ts.map +1 -0
- package/dist/forms-core/elements.js +68 -0
- package/dist/forms-core/elements.js.map +1 -0
- package/dist/forms-core/email-domains.d.ts +10 -0
- package/dist/forms-core/email-domains.d.ts.map +1 -0
- package/dist/forms-core/email-domains.js +69 -0
- package/dist/forms-core/email-domains.js.map +1 -0
- package/dist/forms-core/file-rules.d.ts +51 -0
- package/dist/forms-core/file-rules.d.ts.map +1 -0
- package/dist/forms-core/file-rules.js +105 -0
- package/dist/forms-core/file-rules.js.map +1 -0
- package/dist/forms-core/formula.d.ts +114 -0
- package/dist/forms-core/formula.d.ts.map +1 -0
- package/dist/forms-core/formula.js +1170 -0
- package/dist/forms-core/formula.js.map +1 -0
- package/dist/forms-core/index.d.ts +24 -0
- package/dist/forms-core/index.d.ts.map +1 -0
- package/dist/forms-core/index.js +28 -0
- package/dist/forms-core/index.js.map +1 -0
- package/dist/forms-core/units.d.ts +49 -0
- package/dist/forms-core/units.d.ts.map +1 -0
- package/dist/forms-core/units.js +255 -0
- package/dist/forms-core/units.js.map +1 -0
- package/dist/forms-core/us-states.d.ts +14 -0
- package/dist/forms-core/us-states.d.ts.map +1 -0
- package/dist/forms-core/us-states.js +66 -0
- package/dist/forms-core/us-states.js.map +1 -0
- package/dist/forms-core/validation.d.ts +266 -0
- package/dist/forms-core/validation.d.ts.map +1 -0
- package/dist/forms-core/validation.js +850 -0
- package/dist/forms-core/validation.js.map +1 -0
- package/dist/forms.d.ts +19 -0
- package/dist/forms.d.ts.map +1 -0
- package/dist/forms.js +29 -0
- package/dist/forms.js.map +1 -0
- package/dist/gallery-types.d.ts +32 -0
- package/dist/gallery-types.d.ts.map +1 -0
- package/dist/gallery-types.js +2 -0
- package/dist/gallery-types.js.map +1 -0
- package/dist/graphql.d.ts +97 -0
- package/dist/graphql.d.ts.map +1 -0
- package/dist/graphql.js +212 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.cjs +19448 -0
- package/dist/index.d.cts +19 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/marketing-schema.d.ts +216 -0
- package/dist/marketing-schema.d.ts.map +1 -0
- package/dist/marketing-schema.js +200 -0
- package/dist/marketing-schema.js.map +1 -0
- package/dist/mcp.d.ts +42 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +87 -0
- package/dist/mcp.js.map +1 -0
- package/dist/native.d.ts +45 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +43 -0
- package/dist/native.js.map +1 -0
- package/dist/next-revalidate.d.ts +43 -0
- package/dist/next-revalidate.d.ts.map +1 -0
- package/dist/next-revalidate.js +84 -0
- package/dist/next-revalidate.js.map +1 -0
- package/dist/parallel-pages.d.ts +102 -0
- package/dist/parallel-pages.d.ts.map +1 -0
- package/dist/parallel-pages.js +108 -0
- package/dist/parallel-pages.js.map +1 -0
- package/dist/payment-icons.d.ts +66 -0
- package/dist/payment-icons.d.ts.map +1 -0
- package/dist/payment-icons.js +160 -0
- package/dist/payment-icons.js.map +1 -0
- package/dist/proxy.d.ts +67 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +97 -0
- package/dist/proxy.js.map +1 -0
- package/dist/react-analytics.d.ts +39 -0
- package/dist/react-analytics.d.ts.map +1 -0
- package/dist/react-analytics.js +59 -0
- package/dist/react-analytics.js.map +1 -0
- package/dist/react-attribution.d.ts +99 -0
- package/dist/react-attribution.d.ts.map +1 -0
- package/dist/react-attribution.js +52 -0
- package/dist/react-attribution.js.map +1 -0
- package/dist/react-marketing.d.ts +181 -0
- package/dist/react-marketing.d.ts.map +1 -0
- package/dist/react-marketing.js +215 -0
- package/dist/react-marketing.js.map +1 -0
- package/dist/react.d.ts +727 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +2418 -0
- package/dist/react.js.map +1 -0
- package/dist/reserved-paths.d.ts +24 -0
- package/dist/reserved-paths.d.ts.map +1 -0
- package/dist/reserved-paths.js +60 -0
- package/dist/reserved-paths.js.map +1 -0
- package/dist/revalidate-verify.d.ts +30 -0
- package/dist/revalidate-verify.d.ts.map +1 -0
- package/dist/revalidate-verify.js +74 -0
- package/dist/revalidate-verify.js.map +1 -0
- package/dist/sdk.d.ts +678 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +801 -0
- package/dist/sdk.js.map +1 -0
- package/dist/seo.d.ts +218 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +309 -0
- package/dist/seo.js.map +1 -0
- package/dist/socials.d.ts +47 -0
- package/dist/socials.d.ts.map +1 -0
- package/dist/socials.js +172 -0
- package/dist/socials.js.map +1 -0
- package/dist/static.d.ts +14 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +14 -0
- package/dist/static.js.map +1 -0
- package/dist/url-pattern.d.ts +47 -0
- package/dist/url-pattern.d.ts.map +1 -0
- package/dist/url-pattern.js +124 -0
- package/dist/url-pattern.js.map +1 -0
- package/dist/vanilla.d.ts +77 -0
- package/dist/vanilla.d.ts.map +1 -0
- package/dist/vanilla.js +903 -0
- package/dist/vanilla.js.map +1 -0
- package/openapi.json +13779 -0
- package/package.json +204 -0
- package/src/adapters-revalidate.ts +88 -0
- package/src/alpha.ts +89 -0
- package/src/analytics-core.ts +175 -0
- package/src/analytics-server.ts +99 -0
- package/src/angular/biab-form.component.ts +1443 -0
- package/src/angular/index.ts +26 -0
- package/src/attribution-logo.ts +5 -0
- package/src/auth-handler.ts +352 -0
- package/src/cli.ts +2517 -0
- package/src/client.ts +3578 -0
- package/src/collections.ts +101 -0
- package/src/contracts.ts +3390 -0
- package/src/data-model-schema.ts +4042 -0
- package/src/distribution.ts +73 -0
- package/src/element.ts +321 -0
- package/src/enums.ts +65 -0
- package/src/errors.ts +338 -0
- package/src/forms-core/autocomplete.ts +97 -0
- package/src/forms-core/business-function.ts +209 -0
- package/src/forms-core/calculation.ts +167 -0
- package/src/forms-core/controller.ts +922 -0
- package/src/forms-core/elements.ts +86 -0
- package/src/forms-core/email-domains.ts +68 -0
- package/src/forms-core/file-rules.ts +123 -0
- package/src/forms-core/formula.ts +1339 -0
- package/src/forms-core/index.ts +122 -0
- package/src/forms-core/units.ts +308 -0
- package/src/forms-core/us-states.ts +68 -0
- package/src/forms-core/validation.ts +1037 -0
- package/src/forms.ts +63 -0
- package/src/gallery-types.ts +31 -0
- package/src/graphql.ts +295 -0
- package/src/index.ts +505 -0
- package/src/marketing-schema.ts +395 -0
- package/src/mcp.ts +108 -0
- package/src/native.ts +73 -0
- package/src/next-revalidate.ts +124 -0
- package/src/parallel-pages.ts +192 -0
- package/src/payment-icons.ts +205 -0
- package/src/proxy.ts +173 -0
- package/src/qwik.tsx +1234 -0
- package/src/react-analytics.tsx +76 -0
- package/src/react-attribution.tsx +192 -0
- package/src/react-marketing.tsx +338 -0
- package/src/react.tsx +5495 -0
- package/src/reserved-paths.ts +65 -0
- package/src/revalidate-verify.ts +98 -0
- package/src/sdk.ts +1671 -0
- package/src/seo.ts +488 -0
- package/src/socials.ts +230 -0
- package/src/solid.tsx +1466 -0
- package/src/static.ts +21 -0
- package/src/styles/biab-forms.css +484 -0
- package/src/svelte/BiabForm.svelte +1069 -0
- package/src/svelte/context.ts +57 -0
- package/src/svelte/index.ts +79 -0
- package/src/url-pattern.ts +153 -0
- package/src/vanilla.ts +1290 -0
- package/src/vue/BiabForm.vue +1538 -0
- package/src/vue/index.ts +119 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type InitBiabAnalyticsOptions,
|
|
7
|
+
initBiabAnalytics,
|
|
8
|
+
} from "./analytics-core.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* <BIABAnalytics /> — privacy-conscious site analytics for React
|
|
12
|
+
* SDK consumers. Thin wrapper over `initBiabAnalytics()`; non-React
|
|
13
|
+
* consumers can import that function directly from
|
|
14
|
+
* `@businessdash/sdk/analytics-core`.
|
|
15
|
+
*
|
|
16
|
+
* Drop into the root layout once:
|
|
17
|
+
*
|
|
18
|
+
* import { BIABAnalytics } from "@businessdash/sdk/react-analytics";
|
|
19
|
+
*
|
|
20
|
+
* <BIABAnalytics
|
|
21
|
+
* siteId={process.env.BIAB_SITE_ID!}
|
|
22
|
+
* baseUrl={process.env.BIAB_PACKAGE_API_BASE_URL!}
|
|
23
|
+
* apiKey={process.env.NEXT_PUBLIC_BIAB_PUBLIC_KEY!}
|
|
24
|
+
* />
|
|
25
|
+
*
|
|
26
|
+
* What it does + privacy posture: see analytics-core.ts.
|
|
27
|
+
*/
|
|
28
|
+
export function BIABAnalytics(props: InitBiabAnalyticsOptions) {
|
|
29
|
+
// Destructure so the effect closes over the individual config values (its real
|
|
30
|
+
// dependencies) rather than the `props` object identity, which changes every render.
|
|
31
|
+
const { siteId, baseUrl, apiKey, respectDnt } = props;
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const tracker = initBiabAnalytics({
|
|
34
|
+
siteId,
|
|
35
|
+
baseUrl,
|
|
36
|
+
apiKey,
|
|
37
|
+
...(respectDnt !== undefined ? { respectDnt } : {}),
|
|
38
|
+
});
|
|
39
|
+
return () => tracker.stop();
|
|
40
|
+
}, [siteId, baseUrl, apiKey, respectDnt]);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** BusinessDash-era name for `<BIABAnalytics />` — same component; the
|
|
45
|
+
* original export stays for every existing consumer. */
|
|
46
|
+
export const BusinessDashAnalytics = BIABAnalytics;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fire a custom event (e.g. "newsletter_signup_clicked") from
|
|
50
|
+
* anywhere in the consumer site. The first call boots a tracker
|
|
51
|
+
* if one isn't already running on this page; subsequent calls
|
|
52
|
+
* piggyback on the existing one.
|
|
53
|
+
*/
|
|
54
|
+
export function trackBiabEvent(input: {
|
|
55
|
+
siteId: string;
|
|
56
|
+
baseUrl: string;
|
|
57
|
+
apiKey: string;
|
|
58
|
+
customEventName: string;
|
|
59
|
+
path?: string;
|
|
60
|
+
}): void {
|
|
61
|
+
if (typeof window === "undefined") return;
|
|
62
|
+
const tracker = initBiabAnalytics({
|
|
63
|
+
siteId: input.siteId,
|
|
64
|
+
baseUrl: input.baseUrl,
|
|
65
|
+
apiKey: input.apiKey,
|
|
66
|
+
});
|
|
67
|
+
tracker.track(
|
|
68
|
+
input.customEventName,
|
|
69
|
+
input.path ? { path: input.path } : undefined,
|
|
70
|
+
);
|
|
71
|
+
tracker.stop();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type { InitBiabAnalyticsOptions } from "./analytics-core.js";
|
|
75
|
+
// Re-export the core for non-React consumers convenience.
|
|
76
|
+
export { initBiabAnalytics } from "./analytics-core.js";
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { BUSINESS_DASH_LOGO } from "./attribution-logo.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The "Powered by BusinessDash" attribution badge.
|
|
5
|
+
*
|
|
6
|
+
* Two exports:
|
|
7
|
+
* - {@link BusinessDashFooterBanner} — the presentational badge. Pure, server-
|
|
8
|
+
* renderable, no data fetching. Drop it in and pass `siteId`.
|
|
9
|
+
* - {@link BusinessDashFooter} — an async Server Component that reads the site's
|
|
10
|
+
* attribution policy first, so it renders the badge when required and lets a
|
|
11
|
+
* paid plan hide it. Prefer this in a Next.js Server Component.
|
|
12
|
+
*
|
|
13
|
+
* ── Why server-render it ──
|
|
14
|
+
*
|
|
15
|
+
* BusinessDash verifies the badge by fetching your live domain and looking for
|
|
16
|
+
* the backlink below. If the badge is server-rendered it's in the raw HTML the
|
|
17
|
+
* check sees; if it's client-only, the check would need a headless browser to
|
|
18
|
+
* find it. So rendering it server-side isn't about stopping tampering (you own
|
|
19
|
+
* your code) — it's about the badge being *visible to the verification*.
|
|
20
|
+
*
|
|
21
|
+
* The `?ref=<siteId>` on the link is that marker. Keep it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
type Variation = "block" | "inline" | "none";
|
|
25
|
+
|
|
26
|
+
export interface BusinessDashFooterBannerProps {
|
|
27
|
+
/**
|
|
28
|
+
* Your BusinessDash site id. Used to build the verifiable backlink
|
|
29
|
+
* (`biab.app/?ref=<siteId>`). Omitting it renders a plain link with no marker
|
|
30
|
+
* — fine visually, but a verification crawl won't credit it.
|
|
31
|
+
*/
|
|
32
|
+
siteId?: string;
|
|
33
|
+
/** Company/brand name shown after the © symbol. */
|
|
34
|
+
companyName?: string;
|
|
35
|
+
/** Year shown after the © symbol. Defaults to the current year. */
|
|
36
|
+
year?: number;
|
|
37
|
+
/** Layout variation. Defaults to "block". */
|
|
38
|
+
variation?: Variation;
|
|
39
|
+
/**
|
|
40
|
+
* Override the link target. Defaults to `https://biab.app/?ref=<siteId>` (or
|
|
41
|
+
* `https://biab.app` without a siteId). Only override if you know you want to
|
|
42
|
+
* — a non-biab.app href won't verify.
|
|
43
|
+
*/
|
|
44
|
+
href?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Render nothing. Handy for a plan that's allowed to remove the badge — but
|
|
47
|
+
* note removal is only *permitted*, not enforced, by this prop. See
|
|
48
|
+
* {@link BusinessDashFooter}, which sets it from the live policy.
|
|
49
|
+
*/
|
|
50
|
+
hidden?: boolean;
|
|
51
|
+
/** Extra classes applied to the outer wrapper. */
|
|
52
|
+
className?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The default link: the verifiable backlink when we have a siteId. Exported for
|
|
57
|
+
* tests — the `?ref` marker is what a verification crawl matches on, so it's the
|
|
58
|
+
* one bit worth pinning.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveHref(siteId?: string, href?: string): string {
|
|
61
|
+
if (href) return href;
|
|
62
|
+
return siteId
|
|
63
|
+
? `https://biab.app/?ref=${encodeURIComponent(siteId)}`
|
|
64
|
+
: "https://biab.app";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function BusinessDashFooterBanner({
|
|
68
|
+
siteId,
|
|
69
|
+
companyName = "Your Company",
|
|
70
|
+
year = new Date().getFullYear(),
|
|
71
|
+
variation = "block",
|
|
72
|
+
href,
|
|
73
|
+
hidden = false,
|
|
74
|
+
className = "",
|
|
75
|
+
}: BusinessDashFooterBannerProps) {
|
|
76
|
+
if (hidden || variation === "none") return null;
|
|
77
|
+
|
|
78
|
+
const linkHref = resolveHref(siteId, href);
|
|
79
|
+
|
|
80
|
+
const content = (
|
|
81
|
+
<>
|
|
82
|
+
<span className="whitespace-nowrap text-zinc-500">
|
|
83
|
+
<span className="font-bold">©</span> {year} {companyName}
|
|
84
|
+
</span>
|
|
85
|
+
<span className="text-zinc-600">·</span>
|
|
86
|
+
<span className="flex items-center gap-1 whitespace-nowrap text-zinc-600">
|
|
87
|
+
Powered by
|
|
88
|
+
<a
|
|
89
|
+
aria-label="BusinessDash"
|
|
90
|
+
className="inline-flex cursor-pointer items-center opacity-90 transition-opacity hover:opacity-100"
|
|
91
|
+
href={linkHref}
|
|
92
|
+
rel="noopener"
|
|
93
|
+
target="_blank"
|
|
94
|
+
>
|
|
95
|
+
{/* A plain <img>, not next/image: this ships to any React app,
|
|
96
|
+
and the logo is an inline data URI with nothing to optimize. */}
|
|
97
|
+
<img
|
|
98
|
+
alt="BusinessDash"
|
|
99
|
+
className="h-[24px] w-auto"
|
|
100
|
+
src={BUSINESS_DASH_LOGO}
|
|
101
|
+
/>
|
|
102
|
+
</a>
|
|
103
|
+
</span>
|
|
104
|
+
</>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (variation === "inline") {
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
{/* Desktop: centered along the bottom of a relative footer */}
|
|
111
|
+
<div
|
|
112
|
+
className={`absolute bottom-0 left-0 hidden w-screen flex-wrap items-center justify-center gap-x-3 gap-y-1 pb-1 text-[13px] md:flex ${className}`}
|
|
113
|
+
>
|
|
114
|
+
{content}
|
|
115
|
+
</div>
|
|
116
|
+
{/* Small screens: static, centered */}
|
|
117
|
+
<div
|
|
118
|
+
className={`mx-auto flex w-fit flex-wrap items-center justify-center gap-x-3 gap-y-1 pb-1 text-[13px] md:hidden ${className}`}
|
|
119
|
+
>
|
|
120
|
+
{content}
|
|
121
|
+
</div>
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// block (default)
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
className={`mx-auto flex w-fit flex-wrap items-center justify-center gap-x-3 gap-y-1 text-[13px] ${className}`}
|
|
130
|
+
>
|
|
131
|
+
{content}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default BusinessDashFooterBanner;
|
|
137
|
+
|
|
138
|
+
/** The slice of the site client this component needs — kept structural so it
|
|
139
|
+
* doesn't drag the whole client type in. */
|
|
140
|
+
export interface AttributionPolicyReader {
|
|
141
|
+
attribution: {
|
|
142
|
+
policy: () => Promise<{ siteId: string; required: boolean }>;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface BusinessDashFooterProps
|
|
147
|
+
extends Omit<BusinessDashFooterBannerProps, "siteId" | "hidden"> {
|
|
148
|
+
/** A configured site client, e.g. `biab.site(siteId)`. */
|
|
149
|
+
client: AttributionPolicyReader;
|
|
150
|
+
/**
|
|
151
|
+
* Ask to hide the badge. Honored ONLY when the plan permits removal
|
|
152
|
+
* (`policy.required === false`); on a plan that requires the badge this is
|
|
153
|
+
* ignored and the badge shows. Defaults to `false` (always show).
|
|
154
|
+
*/
|
|
155
|
+
optOut?: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Policy-aware badge for a Next.js Server Component.
|
|
160
|
+
*
|
|
161
|
+
* Reads the live attribution policy, then renders {@link BusinessDashFooterBanner}
|
|
162
|
+
* with the right `siteId` (for the marker) and an auto-resolved `hidden`: a paid
|
|
163
|
+
* plan that passed `optOut` gets a blank; a plan that requires the badge always
|
|
164
|
+
* shows it, `optOut` or not.
|
|
165
|
+
*
|
|
166
|
+
* ```tsx
|
|
167
|
+
* // app/layout.tsx (Server Component)
|
|
168
|
+
* export default async function Layout({ children }) {
|
|
169
|
+
* return (
|
|
170
|
+
* <>
|
|
171
|
+
* {children}
|
|
172
|
+
* <BusinessDashFooter client={biab.site(SITE_ID)} companyName="Acme Co" />
|
|
173
|
+
* </>
|
|
174
|
+
* )
|
|
175
|
+
* }
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
export async function BusinessDashFooter({
|
|
179
|
+
client,
|
|
180
|
+
optOut = false,
|
|
181
|
+
...bannerProps
|
|
182
|
+
}: BusinessDashFooterProps) {
|
|
183
|
+
const policy = await client.attribution.policy();
|
|
184
|
+
const hidden = optOut && !policy.required;
|
|
185
|
+
return (
|
|
186
|
+
<BusinessDashFooterBanner
|
|
187
|
+
{...bannerProps}
|
|
188
|
+
hidden={hidden}
|
|
189
|
+
siteId={policy.siteId}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hooks for the schema-driven marketing-page flow.
|
|
3
|
+
*
|
|
4
|
+
* The base SDK file (`react.tsx`) is already past the workspace's
|
|
5
|
+
* "stop and ask before growing" file-size threshold, so these new hooks
|
|
6
|
+
* live alongside it in their own module. Same import target for
|
|
7
|
+
* consumers either way — both files are re-exported from `react.tsx`
|
|
8
|
+
* via the SDK's `./react` entrypoint.
|
|
9
|
+
*
|
|
10
|
+
* Hooks here intentionally bypass `useEffect`-managed sessions (the
|
|
11
|
+
* approach the older iframe surfaces use) — marketing data is plain
|
|
12
|
+
* JSON-over-fetch with process-wide caching on the client. That keeps
|
|
13
|
+
* the SSR story simple: the same `client.site(siteId).marketing.*`
|
|
14
|
+
* methods consumed in `getServerSideProps` / RSC code paths are what
|
|
15
|
+
* these hooks call client-side.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
19
|
+
|
|
20
|
+
import { BiabDevClient } from "./client.js";
|
|
21
|
+
import type {
|
|
22
|
+
GetMarketingLocalesResponse,
|
|
23
|
+
GetMarketingPageBundleResponse,
|
|
24
|
+
GetPublishedMarketingSchemaResponse,
|
|
25
|
+
SiteMarketingSectionEntry,
|
|
26
|
+
} from "./contracts.js";
|
|
27
|
+
|
|
28
|
+
// ── Bundle hook ─────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
type BundleState =
|
|
31
|
+
| { status: "idle"; bundle: null; error: null }
|
|
32
|
+
| { status: "loading"; bundle: null; error: null }
|
|
33
|
+
| { status: "ready"; bundle: GetMarketingPageBundleResponse; error: null }
|
|
34
|
+
| { status: "error"; bundle: null; error: Error };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Live-site bundle reader. One call returns:
|
|
38
|
+
* - `sections`: `Record<sectionKey, {ok:true,data,source} | {ok:false,error}>`
|
|
39
|
+
* — a malformed section degrades to empty in the consumer's renderer
|
|
40
|
+
* instead of crashing the page.
|
|
41
|
+
* - `seo`: a fully-shaped SEO object with brand tokens already expanded.
|
|
42
|
+
* - `availableLocales` for the language switcher.
|
|
43
|
+
*
|
|
44
|
+
* The hook reads from the SDK's process-wide cache when present
|
|
45
|
+
* (warmed by `prefetchPageBundle()` or an earlier render on the same
|
|
46
|
+
* `(siteId, pageKey, locale)` key) and only hits the network on cache
|
|
47
|
+
* miss. `refresh()` does a `skipCache` round-trip — useful after a
|
|
48
|
+
* dashboard save that the SDK didn't initiate.
|
|
49
|
+
*/
|
|
50
|
+
export function useMarketingPageBundle(options: {
|
|
51
|
+
apiKey: string;
|
|
52
|
+
baseUrl: string;
|
|
53
|
+
siteId: string;
|
|
54
|
+
pageKey?: string;
|
|
55
|
+
locale?: string;
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
}): BundleState & { refresh: () => Promise<void> } {
|
|
58
|
+
const {
|
|
59
|
+
apiKey,
|
|
60
|
+
baseUrl,
|
|
61
|
+
siteId,
|
|
62
|
+
pageKey = "home",
|
|
63
|
+
locale = "en",
|
|
64
|
+
enabled = true,
|
|
65
|
+
} = options;
|
|
66
|
+
const client = useMemo(
|
|
67
|
+
() => new BiabDevClient({ apiKey, baseUrl }),
|
|
68
|
+
[apiKey, baseUrl],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const initial = client
|
|
72
|
+
.site(siteId)
|
|
73
|
+
.marketing.getPageBundleCached({ pageKey, locale });
|
|
74
|
+
const [state, setState] = useState<BundleState>(
|
|
75
|
+
initial
|
|
76
|
+
? { status: "ready", bundle: initial, error: null }
|
|
77
|
+
: { status: "idle", bundle: null, error: null },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const fetchOnce = useCallback(
|
|
81
|
+
async (skipCache = false) => {
|
|
82
|
+
if (!enabled) return;
|
|
83
|
+
setState({ status: "loading", bundle: null, error: null });
|
|
84
|
+
try {
|
|
85
|
+
const response = await client
|
|
86
|
+
.site(siteId)
|
|
87
|
+
.marketing.getPageBundle({ pageKey, locale }, { skipCache });
|
|
88
|
+
setState({ status: "ready", bundle: response, error: null });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
setState({
|
|
91
|
+
status: "error",
|
|
92
|
+
bundle: null,
|
|
93
|
+
error: err instanceof Error ? err : new Error("Bundle load failed"),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[client, siteId, pageKey, locale, enabled],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!enabled) return;
|
|
102
|
+
const hit = client
|
|
103
|
+
.site(siteId)
|
|
104
|
+
.marketing.getPageBundleCached({ pageKey, locale });
|
|
105
|
+
if (hit) {
|
|
106
|
+
setState({ status: "ready", bundle: hit, error: null });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
void fetchOnce(false);
|
|
110
|
+
}, [client, siteId, pageKey, locale, enabled, fetchOnce]);
|
|
111
|
+
|
|
112
|
+
return { ...state, refresh: () => fetchOnce(true) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Single-section convenience hook ─────────────────────────────────
|
|
116
|
+
|
|
117
|
+
type SectionState<T> =
|
|
118
|
+
| { status: "idle" | "loading"; data: null; entry: null; error: null }
|
|
119
|
+
| {
|
|
120
|
+
status: "ready";
|
|
121
|
+
data: T | null;
|
|
122
|
+
entry: SiteMarketingSectionEntry | null;
|
|
123
|
+
error: null;
|
|
124
|
+
}
|
|
125
|
+
| { status: "error"; data: null; entry: null; error: Error };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Pull one section out of the bundle for `(pageKey, locale)`. Pass a
|
|
129
|
+
* `parse` function to validate the section data against the consumer's
|
|
130
|
+
* zod schema (this is what `defineSiteMarketingSchema(...).parseSection(...)`
|
|
131
|
+
* gives you).
|
|
132
|
+
*
|
|
133
|
+
* When the underlying section entry is `{ ok: false }`, `data` resolves
|
|
134
|
+
* to `null` so the consumer renderer can short-circuit to "render
|
|
135
|
+
* empty" without checking the entry shape.
|
|
136
|
+
*/
|
|
137
|
+
export function useMarketingSection<T>(options: {
|
|
138
|
+
apiKey: string;
|
|
139
|
+
baseUrl: string;
|
|
140
|
+
siteId: string;
|
|
141
|
+
pageKey?: string;
|
|
142
|
+
locale?: string;
|
|
143
|
+
sectionKey: string;
|
|
144
|
+
parse?: (entry: SiteMarketingSectionEntry | null | undefined) => T | null;
|
|
145
|
+
enabled?: boolean;
|
|
146
|
+
}): SectionState<T> {
|
|
147
|
+
const { apiKey, baseUrl, siteId, sectionKey, parse } = options;
|
|
148
|
+
|
|
149
|
+
// Build the inner bundle-hook options conditionally so the optional
|
|
150
|
+
// keys aren't present when the consumer didn't pass them. tsc's
|
|
151
|
+
// `exactOptionalPropertyTypes` flag distinguishes `prop: undefined`
|
|
152
|
+
// from "prop absent" and the latter is what the bundle hook expects.
|
|
153
|
+
const bundleOptions: Parameters<typeof useMarketingPageBundle>[0] = {
|
|
154
|
+
apiKey,
|
|
155
|
+
baseUrl,
|
|
156
|
+
siteId,
|
|
157
|
+
};
|
|
158
|
+
if (options.pageKey !== undefined) bundleOptions.pageKey = options.pageKey;
|
|
159
|
+
if (options.locale !== undefined) bundleOptions.locale = options.locale;
|
|
160
|
+
if (options.enabled !== undefined) bundleOptions.enabled = options.enabled;
|
|
161
|
+
|
|
162
|
+
const bundle = useMarketingPageBundle(bundleOptions);
|
|
163
|
+
|
|
164
|
+
return useMemo<SectionState<T>>(() => {
|
|
165
|
+
if (bundle.status === "loading" || bundle.status === "idle") {
|
|
166
|
+
return { status: bundle.status, data: null, entry: null, error: null };
|
|
167
|
+
}
|
|
168
|
+
if (bundle.status === "error") {
|
|
169
|
+
return { status: "error", data: null, entry: null, error: bundle.error };
|
|
170
|
+
}
|
|
171
|
+
const entry = bundle.bundle.sections[sectionKey] ?? null;
|
|
172
|
+
const data: T | null = parse
|
|
173
|
+
? parse(entry)
|
|
174
|
+
: entry?.ok
|
|
175
|
+
? (entry.data as T)
|
|
176
|
+
: null;
|
|
177
|
+
return { status: "ready", data, entry, error: null };
|
|
178
|
+
}, [bundle, sectionKey, parse]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── SEO sub-hook ────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
export function useMarketingPageSeo(options: {
|
|
184
|
+
apiKey: string;
|
|
185
|
+
baseUrl: string;
|
|
186
|
+
siteId: string;
|
|
187
|
+
pageKey?: string;
|
|
188
|
+
locale?: string;
|
|
189
|
+
}) {
|
|
190
|
+
const bundle = useMarketingPageBundle(options);
|
|
191
|
+
return useMemo(() => {
|
|
192
|
+
if (bundle.status !== "ready") {
|
|
193
|
+
return { status: bundle.status, seo: null } as const;
|
|
194
|
+
}
|
|
195
|
+
return { status: "ready" as const, seo: bundle.bundle.seo };
|
|
196
|
+
}, [bundle]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Published schema (rarely changes) ────────────────────────────────
|
|
200
|
+
|
|
201
|
+
type SchemaState =
|
|
202
|
+
| { status: "idle" | "loading"; schema: null; error: null }
|
|
203
|
+
| {
|
|
204
|
+
status: "ready";
|
|
205
|
+
schema: GetPublishedMarketingSchemaResponse["schema"];
|
|
206
|
+
error: null;
|
|
207
|
+
}
|
|
208
|
+
| { status: "error"; schema: null; error: Error };
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Fetch the consumer's *published* schema once per process (cache
|
|
212
|
+
* scoped to `siteId`). Most consumers don't need this in the browser —
|
|
213
|
+
* the schema lives in their own bundle via `defineSiteMarketingSchema`
|
|
214
|
+
* — but it's the right primitive for "preview-as-other-tenant"
|
|
215
|
+
* tooling or for editors that introspect their own published artifact.
|
|
216
|
+
*/
|
|
217
|
+
export function usePublishedMarketingSchema(options: {
|
|
218
|
+
apiKey: string;
|
|
219
|
+
baseUrl: string;
|
|
220
|
+
siteId: string;
|
|
221
|
+
enabled?: boolean;
|
|
222
|
+
}): SchemaState & { refresh: () => Promise<void> } {
|
|
223
|
+
const { apiKey, baseUrl, siteId, enabled = true } = options;
|
|
224
|
+
const client = useMemo(
|
|
225
|
+
() => new BiabDevClient({ apiKey, baseUrl }),
|
|
226
|
+
[apiKey, baseUrl],
|
|
227
|
+
);
|
|
228
|
+
const [state, setState] = useState<SchemaState>({
|
|
229
|
+
status: "idle",
|
|
230
|
+
schema: null,
|
|
231
|
+
error: null,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const fetchOnce = useCallback(
|
|
235
|
+
async (skipCache = false) => {
|
|
236
|
+
if (!enabled) return;
|
|
237
|
+
setState({ status: "loading", schema: null, error: null });
|
|
238
|
+
try {
|
|
239
|
+
const response = await client
|
|
240
|
+
.site(siteId)
|
|
241
|
+
.marketing.getPublishedSchema({ skipCache });
|
|
242
|
+
setState({ status: "ready", schema: response.schema, error: null });
|
|
243
|
+
} catch (err) {
|
|
244
|
+
setState({
|
|
245
|
+
status: "error",
|
|
246
|
+
schema: null,
|
|
247
|
+
error: err instanceof Error ? err : new Error("Schema load failed"),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
[client, siteId, enabled],
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
void fetchOnce(false);
|
|
256
|
+
}, [fetchOnce]);
|
|
257
|
+
|
|
258
|
+
return { ...state, refresh: () => fetchOnce(true) };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Locales + brand tokens (sites with i18n) ─────────────────────────
|
|
262
|
+
|
|
263
|
+
type LocalesState =
|
|
264
|
+
| { status: "idle" | "loading"; data: null; error: null }
|
|
265
|
+
| { status: "ready"; data: GetMarketingLocalesResponse; error: null }
|
|
266
|
+
| { status: "error"; data: null; error: Error };
|
|
267
|
+
|
|
268
|
+
export function useMarketingLocales(options: {
|
|
269
|
+
apiKey: string;
|
|
270
|
+
baseUrl: string;
|
|
271
|
+
siteId: string;
|
|
272
|
+
}): LocalesState {
|
|
273
|
+
const { apiKey, baseUrl, siteId } = options;
|
|
274
|
+
const client = useMemo(
|
|
275
|
+
() => new BiabDevClient({ apiKey, baseUrl }),
|
|
276
|
+
[apiKey, baseUrl],
|
|
277
|
+
);
|
|
278
|
+
const [state, setState] = useState<LocalesState>({
|
|
279
|
+
status: "idle",
|
|
280
|
+
data: null,
|
|
281
|
+
error: null,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Avoid double-fires in strict mode by remembering the last
|
|
285
|
+
// `(siteId, apiKey, baseUrl)` we resolved against.
|
|
286
|
+
const inflight = useRef("");
|
|
287
|
+
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
const fp = `${apiKey}|${baseUrl}|${siteId}`;
|
|
290
|
+
if (inflight.current === fp) return;
|
|
291
|
+
inflight.current = fp;
|
|
292
|
+
setState({ status: "loading", data: null, error: null });
|
|
293
|
+
client
|
|
294
|
+
.site(siteId)
|
|
295
|
+
.marketing.getLocales()
|
|
296
|
+
.then((data) => setState({ status: "ready", data, error: null }))
|
|
297
|
+
.catch((err) =>
|
|
298
|
+
setState({
|
|
299
|
+
status: "error",
|
|
300
|
+
data: null,
|
|
301
|
+
error: err instanceof Error ? err : new Error("Locales load failed"),
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
}, [client, siteId, apiKey, baseUrl]);
|
|
305
|
+
|
|
306
|
+
return state;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Bundle preloader ────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Stable `prefetch({ pageKey, locale })` you can wire to
|
|
313
|
+
* `<Link onPointerEnter>` / `onFocus` so the next page is already
|
|
314
|
+
* loaded by the time the visitor clicks. Idempotent — repeat calls hit
|
|
315
|
+
* the cache.
|
|
316
|
+
*/
|
|
317
|
+
export function useMarketingBundlePreloader(options: {
|
|
318
|
+
apiKey: string;
|
|
319
|
+
baseUrl: string;
|
|
320
|
+
siteId: string;
|
|
321
|
+
}): (input?: { pageKey?: string; locale?: string }) => Promise<void> {
|
|
322
|
+
const { apiKey, baseUrl, siteId } = options;
|
|
323
|
+
const client = useMemo(
|
|
324
|
+
() => new BiabDevClient({ apiKey, baseUrl }),
|
|
325
|
+
[apiKey, baseUrl],
|
|
326
|
+
);
|
|
327
|
+
return useCallback(
|
|
328
|
+
async (input?: { pageKey?: string; locale?: string }) => {
|
|
329
|
+
try {
|
|
330
|
+
await client.site(siteId).marketing.prefetchPageBundle(input);
|
|
331
|
+
} catch {
|
|
332
|
+
// preload failures are non-fatal — the actual page render will
|
|
333
|
+
// surface the error via the bundle hook.
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
[client, siteId],
|
|
337
|
+
);
|
|
338
|
+
}
|