@distinctagency/cms-client 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
1
3
  interface CmsAnalyticsProps {
2
4
  /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */
3
5
  trackingUrl: string;
@@ -65,4 +67,10 @@ interface PageTrackerProps {
65
67
  */
66
68
  declare function PageTracker({ trackingUrl, apiKey, trackScroll, trackTime, }: PageTrackerProps): null;
67
69
 
68
- export { CmsAnalytics, PageTracker };
70
+ interface CmsEmbedProps {
71
+ field: unknown;
72
+ className?: string;
73
+ }
74
+ declare function CmsEmbed({ field, className }: CmsEmbedProps): react_jsx_runtime.JSX.Element | null;
75
+
76
+ export { CmsAnalytics, CmsEmbed, PageTracker };
package/dist/client.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
1
3
  interface CmsAnalyticsProps {
2
4
  /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */
3
5
  trackingUrl: string;
@@ -65,4 +67,10 @@ interface PageTrackerProps {
65
67
  */
66
68
  declare function PageTracker({ trackingUrl, apiKey, trackScroll, trackTime, }: PageTrackerProps): null;
67
69
 
68
- export { CmsAnalytics, PageTracker };
70
+ interface CmsEmbedProps {
71
+ field: unknown;
72
+ className?: string;
73
+ }
74
+ declare function CmsEmbed({ field, className }: CmsEmbedProps): react_jsx_runtime.JSX.Element | null;
75
+
76
+ export { CmsAnalytics, CmsEmbed, PageTracker };
package/dist/client.js CHANGED
@@ -23,6 +23,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
23
23
  var client_exports = {};
24
24
  __export(client_exports, {
25
25
  CmsAnalytics: () => CmsAnalytics,
26
+ CmsEmbed: () => CmsEmbed,
26
27
  PageTracker: () => PageTracker
27
28
  });
28
29
  module.exports = __toCommonJS(client_exports);
@@ -200,9 +201,46 @@ function getSessionId2() {
200
201
  }
201
202
  return sid;
202
203
  }
204
+
205
+ // src/embed-component.tsx
206
+ var import_jsx_runtime = require("react/jsx-runtime");
207
+ function toCssAspectRatio(ratio) {
208
+ const parts = ratio.split(":");
209
+ if (parts.length !== 2) return "16/9";
210
+ const w = parseFloat(parts[0]);
211
+ const h = parseFloat(parts[1]);
212
+ if (!w || !h || w <= 0 || h <= 0) return "16/9";
213
+ return `${w}/${h}`;
214
+ }
215
+ function CmsEmbed({ field, className }) {
216
+ if (!field || typeof field !== "object") return null;
217
+ const embed = field;
218
+ if (!embed.url || !embed.url.startsWith("https://")) return null;
219
+ const width = embed.width || "100%";
220
+ const aspectRatio = toCssAspectRatio(embed.aspect_ratio || "16:9");
221
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
222
+ "div",
223
+ {
224
+ className,
225
+ style: { position: "relative", width, aspectRatio },
226
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
227
+ "iframe",
228
+ {
229
+ src: embed.url,
230
+ style: { position: "absolute", inset: 0, width: "100%", height: "100%" },
231
+ frameBorder: "0",
232
+ sandbox: "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox",
233
+ allowFullScreen: true,
234
+ loading: "lazy"
235
+ }
236
+ )
237
+ }
238
+ );
239
+ }
203
240
  // Annotate the CommonJS export names for ESM import in node:
204
241
  0 && (module.exports = {
205
242
  CmsAnalytics,
243
+ CmsEmbed,
206
244
  PageTracker
207
245
  });
208
246
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/analytics.tsx","../src/page-tracker.tsx"],"sourcesContent":["\"use client\"\n\n// Client-side React components — requires \"use client\" context\nexport { CmsAnalytics } from \"./analytics\"\nexport { PageTracker } from \"./page-tracker\"\n","\"use client\"\n\nimport { useEffect, useRef } from \"react\"\n\ninterface CmsAnalyticsProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Content item ID (from CMS) */\n contentItemId?: string\n /** Content type slug (e.g. \"blog_posts\") */\n contentTypeSlug: string\n /** Item slug (e.g. \"my-post\") */\n itemSlug: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Drop-in analytics component for CMS content pages.\n * Tracks page views, scroll depth, and time on page.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage:\n * ```tsx\n * <CmsAnalytics\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * contentTypeSlug=\"blog_posts\"\n * itemSlug={params.slug}\n * />\n * ```\n */\nexport function CmsAnalytics({\n trackingUrl,\n apiKey,\n contentItemId,\n contentTypeSlug,\n itemSlug,\n trackScroll = true,\n trackTime = true,\n}: CmsAnalyticsProps) {\n const sentRef = useRef(false)\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Only send page view once per mount\n if (sentRef.current) return\n sentRef.current = true\n\n // Page view event\n sendEvent(trackingUrl, {\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"page_view\",\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n })\n\n // Scroll tracking\n let scrollHandler: (() => void) | null = null\n if (trackScroll) {\n scrollHandler = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n window.addEventListener(\"scroll\", scrollHandler, { passive: true })\n }\n\n // Send engagement data on page leave\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const payload = JSON.stringify({\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"engagement\",\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n },\n })\n\n // Use sendBeacon for reliable delivery on page unload\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n if (trackScroll || trackTime) {\n window.addEventListener(\"beforeunload\", handleUnload)\n }\n\n return () => {\n if (scrollHandler) window.removeEventListener(\"scroll\", scrollHandler)\n if (trackScroll || trackTime) window.removeEventListener(\"beforeunload\", handleUnload)\n }\n }, [trackingUrl, apiKey, contentItemId, contentTypeSlug, itemSlug, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {}) // fire and forget\n}\n\n/** Generate a random session ID (persists for the browser session) */\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n","\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { usePathname } from \"next/navigation\"\n\ninterface PageTrackerProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Site-wide page tracker. Add once in root layout to track all pages.\n * Automatically detects CMS content pages from URL structure.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage in src/app/layout.tsx:\n * ```tsx\n * import { PageTracker } from \"@distinctagency/cms-client/client\"\n *\n * <body>\n * {children}\n * <PageTracker\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * />\n * </body>\n * ```\n */\nexport function PageTracker({\n trackingUrl,\n apiKey,\n trackScroll = true,\n trackTime = true,\n}: PageTrackerProps) {\n const pathname = usePathname()\n const prevPathRef = useRef(\"\")\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Skip if same path (prevents double-fire on mount)\n if (pathname === prevPathRef.current) return\n prevPathRef.current = pathname\n\n // Reset engagement tracking for new page\n maxScrollRef.current = 0\n startTimeRef.current = Date.now()\n\n // Try to extract content type and item slug from URL\n // Supports patterns like /events/future-finance, /blog/my-post, etc.\n const segments = pathname.split(\"/\").filter(Boolean)\n const contentTypeSlug = segments[0] ?? null\n const itemSlug = segments.length >= 2 ? segments[segments.length - 1] : null\n\n // Fire page view\n sendEvent(trackingUrl, {\n api_key: apiKey,\n event_type: \"page_view\",\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n metadata: { path: pathname },\n })\n }, [pathname, trackingUrl, apiKey])\n\n // Scroll tracking\n useEffect(() => {\n if (!trackScroll) return\n\n const handleScroll = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n\n window.addEventListener(\"scroll\", handleScroll, { passive: true })\n return () => window.removeEventListener(\"scroll\", handleScroll)\n }, [trackScroll])\n\n // Send engagement data on page leave\n useEffect(() => {\n if (!trackScroll && !trackTime) return\n\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const segments = prevPathRef.current.split(\"/\").filter(Boolean)\n\n const payload = JSON.stringify({\n api_key: apiKey,\n event_type: \"engagement\",\n content_type_slug: segments[0] ?? null,\n item_slug: segments.length >= 2 ? segments[segments.length - 1] : null,\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n path: prevPathRef.current,\n },\n })\n\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n window.addEventListener(\"beforeunload\", handleUnload)\n return () => window.removeEventListener(\"beforeunload\", handleUnload)\n }, [trackingUrl, apiKey, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {})\n}\n\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n"],"mappings":"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","names":["import_react","getSessionId","sendEvent"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/analytics.tsx","../src/page-tracker.tsx","../src/embed-component.tsx"],"sourcesContent":["\"use client\"\n\n// Client-side React components — requires \"use client\" context\nexport { CmsAnalytics } from \"./analytics\"\nexport { PageTracker } from \"./page-tracker\"\nexport { CmsEmbed } from \"./embed-component\"\n","\"use client\"\n\nimport { useEffect, useRef } from \"react\"\n\ninterface CmsAnalyticsProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Content item ID (from CMS) */\n contentItemId?: string\n /** Content type slug (e.g. \"blog_posts\") */\n contentTypeSlug: string\n /** Item slug (e.g. \"my-post\") */\n itemSlug: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Drop-in analytics component for CMS content pages.\n * Tracks page views, scroll depth, and time on page.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage:\n * ```tsx\n * <CmsAnalytics\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * contentTypeSlug=\"blog_posts\"\n * itemSlug={params.slug}\n * />\n * ```\n */\nexport function CmsAnalytics({\n trackingUrl,\n apiKey,\n contentItemId,\n contentTypeSlug,\n itemSlug,\n trackScroll = true,\n trackTime = true,\n}: CmsAnalyticsProps) {\n const sentRef = useRef(false)\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Only send page view once per mount\n if (sentRef.current) return\n sentRef.current = true\n\n // Page view event\n sendEvent(trackingUrl, {\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"page_view\",\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n })\n\n // Scroll tracking\n let scrollHandler: (() => void) | null = null\n if (trackScroll) {\n scrollHandler = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n window.addEventListener(\"scroll\", scrollHandler, { passive: true })\n }\n\n // Send engagement data on page leave\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const payload = JSON.stringify({\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"engagement\",\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n },\n })\n\n // Use sendBeacon for reliable delivery on page unload\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n if (trackScroll || trackTime) {\n window.addEventListener(\"beforeunload\", handleUnload)\n }\n\n return () => {\n if (scrollHandler) window.removeEventListener(\"scroll\", scrollHandler)\n if (trackScroll || trackTime) window.removeEventListener(\"beforeunload\", handleUnload)\n }\n }, [trackingUrl, apiKey, contentItemId, contentTypeSlug, itemSlug, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {}) // fire and forget\n}\n\n/** Generate a random session ID (persists for the browser session) */\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n","\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { usePathname } from \"next/navigation\"\n\ninterface PageTrackerProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Site-wide page tracker. Add once in root layout to track all pages.\n * Automatically detects CMS content pages from URL structure.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage in src/app/layout.tsx:\n * ```tsx\n * import { PageTracker } from \"@distinctagency/cms-client/client\"\n *\n * <body>\n * {children}\n * <PageTracker\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * />\n * </body>\n * ```\n */\nexport function PageTracker({\n trackingUrl,\n apiKey,\n trackScroll = true,\n trackTime = true,\n}: PageTrackerProps) {\n const pathname = usePathname()\n const prevPathRef = useRef(\"\")\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Skip if same path (prevents double-fire on mount)\n if (pathname === prevPathRef.current) return\n prevPathRef.current = pathname\n\n // Reset engagement tracking for new page\n maxScrollRef.current = 0\n startTimeRef.current = Date.now()\n\n // Try to extract content type and item slug from URL\n // Supports patterns like /events/future-finance, /blog/my-post, etc.\n const segments = pathname.split(\"/\").filter(Boolean)\n const contentTypeSlug = segments[0] ?? null\n const itemSlug = segments.length >= 2 ? segments[segments.length - 1] : null\n\n // Fire page view\n sendEvent(trackingUrl, {\n api_key: apiKey,\n event_type: \"page_view\",\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n metadata: { path: pathname },\n })\n }, [pathname, trackingUrl, apiKey])\n\n // Scroll tracking\n useEffect(() => {\n if (!trackScroll) return\n\n const handleScroll = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n\n window.addEventListener(\"scroll\", handleScroll, { passive: true })\n return () => window.removeEventListener(\"scroll\", handleScroll)\n }, [trackScroll])\n\n // Send engagement data on page leave\n useEffect(() => {\n if (!trackScroll && !trackTime) return\n\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const segments = prevPathRef.current.split(\"/\").filter(Boolean)\n\n const payload = JSON.stringify({\n api_key: apiKey,\n event_type: \"engagement\",\n content_type_slug: segments[0] ?? null,\n item_slug: segments.length >= 2 ? segments[segments.length - 1] : null,\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n path: prevPathRef.current,\n },\n })\n\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n window.addEventListener(\"beforeunload\", handleUnload)\n return () => window.removeEventListener(\"beforeunload\", handleUnload)\n }, [trackingUrl, apiKey, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {})\n}\n\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport type { EmbedValue } from \"./types\"\n\ninterface CmsEmbedProps {\n field: unknown\n className?: string\n}\n\nfunction toCssAspectRatio(ratio: string): string {\n const parts = ratio.split(\":\")\n if (parts.length !== 2) return \"16/9\"\n const w = parseFloat(parts[0])\n const h = parseFloat(parts[1])\n if (!w || !h || w <= 0 || h <= 0) return \"16/9\"\n return `${w}/${h}`\n}\n\nexport function CmsEmbed({ field, className }: CmsEmbedProps) {\n if (!field || typeof field !== \"object\") return null\n const embed = field as EmbedValue\n if (!embed.url || !embed.url.startsWith(\"https://\")) return null\n\n const width = embed.width || \"100%\"\n const aspectRatio = toCssAspectRatio(embed.aspect_ratio || \"16:9\")\n\n return (\n <div\n className={className}\n style={{ position: \"relative\", width, aspectRatio }}\n >\n <iframe\n src={embed.url}\n style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\" }}\n frameBorder=\"0\"\n sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox\"\n allowFullScreen\n loading=\"lazy\"\n />\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,mBAAkC;AAoC3B,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,YAAY;AACd,GAAsB;AACpB,QAAM,cAAU,qBAAO,KAAK;AAC5B,QAAM,mBAAe,qBAAO,CAAC;AAC7B,QAAM,mBAAe,qBAAO,KAAK,IAAI,CAAC;AACtC,QAAM,mBAAe,qBAAO,aAAa,CAAC;AAE1C,8BAAU,MAAM;AAEd,QAAI,QAAQ,QAAS;AACrB,YAAQ,UAAU;AAGlB,cAAU,aAAa;AAAA,MACrB,SAAS;AAAA,MACT,iBAAiB;AAAA,MACjB,mBAAmB;AAAA,MACnB,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,UAAU,SAAS,YAAY;AAAA,MAC/B,YAAY,aAAa;AAAA,IAC3B,CAAC;AAGD,QAAI,gBAAqC;AACzC,QAAI,aAAa;AACf,sBAAgB,MAAM;AACpB,cAAM,eAAe,SAAS,gBAAgB,eAAe,OAAO;AACpE,YAAI,gBAAgB,EAAG;AACvB,cAAM,MAAM,KAAK,MAAO,OAAO,UAAU,eAAgB,GAAG;AAC5D,YAAI,MAAM,aAAa,QAAS,cAAa,UAAU;AAAA,MACzD;AACA,aAAO,iBAAiB,UAAU,eAAe,EAAE,SAAS,KAAK,CAAC;AAAA,IACpE;AAGA,UAAM,eAAe,MAAM;AACzB,YAAM,aAAa,KAAK,OAAO,KAAK,IAAI,IAAI,aAAa,WAAW,GAAI;AACxE,YAAM,UAAU,KAAK,UAAU;AAAA,QAC7B,SAAS;AAAA,QACT,iBAAiB;AAAA,QACjB,mBAAmB;AAAA,QACnB,WAAW;AAAA,QACX,YAAY;AAAA,QACZ,YAAY,aAAa;AAAA,QACzB,UAAU;AAAA,UACR,cAAc,aAAa;AAAA,UAC3B,cAAc;AAAA,QAChB;AAAA,MACF,CAAC;AAGD,UAAI,UAAU,YAAY;AACxB,kBAAU,WAAW,aAAa,OAAO;AAAA,MAC3C;AAAA,IACF;AAEA,QAAI,eAAe,WAAW;AAC5B,aAAO,iBAAiB,gBAAgB,YAAY;AAAA,IACtD;AAEA,WAAO,MAAM;AACX,UAAI,cAAe,QAAO,oBAAoB,UAAU,aAAa;AACrE,UAAI,eAAe,UAAW,QAAO,oBAAoB,gBAAgB,YAAY;AAAA,IACvF;AAAA,EACF,GAAG,CAAC,aAAa,QAAQ,eAAe,iBAAiB,UAAU,aAAa,SAAS,CAAC;AAE1F,SAAO;AACT;AAEA,SAAS,UAAU,KAAa,MAA+B;AAC7D,QAAM,KAAK;AAAA,IACT,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU,IAAI;AAAA,IACzB,WAAW;AAAA,EACb,CAAC,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AACnB;AAGA,SAAS,eAAuB;AAC9B,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,MAAM;AACZ,MAAI,MAAM,eAAe,QAAQ,GAAG;AACpC,MAAI,CAAC,KAAK;AACR,UAAM,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE;AAClE,mBAAe,QAAQ,KAAK,GAAG;AAAA,EACjC;AACA,SAAO;AACT;;;ACpIA,IAAAA,gBAAkC;AAClC,wBAA4B;AAiCrB,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,YAAY;AACd,GAAqB;AACnB,QAAM,eAAW,+BAAY;AAC7B,QAAM,kBAAc,sBAAO,EAAE;AAC7B,QAAM,mBAAe,sBAAO,CAAC;AAC7B,QAAM,mBAAe,sBAAO,KAAK,IAAI,CAAC;AACtC,QAAM,mBAAe,sBAAOC,cAAa,CAAC;AAE1C,+BAAU,MAAM;AAEd,QAAI,aAAa,YAAY,QAAS;AACtC,gBAAY,UAAU;AAGtB,iBAAa,UAAU;AACvB,iBAAa,UAAU,KAAK,IAAI;AAIhC,UAAM,WAAW,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AACnD,UAAM,kBAAkB,SAAS,CAAC,KAAK;AACvC,UAAM,WAAW,SAAS,UAAU,IAAI,SAAS,SAAS,SAAS,CAAC,IAAI;AAGxE,IAAAC,WAAU,aAAa;AAAA,MACrB,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,mBAAmB;AAAA,MACnB,WAAW;AAAA,MACX,UAAU,SAAS,YAAY;AAAA,MAC/B,YAAY,aAAa;AAAA,MACzB,UAAU,EAAE,MAAM,SAAS;AAAA,IAC7B,CAAC;AAAA,EACH,GAAG,CAAC,UAAU,aAAa,MAAM,CAAC;AAGlC,+BAAU,MAAM;AACd,QAAI,CAAC,YAAa;AAElB,UAAM,eAAe,MAAM;AACzB,YAAM,eAAe,SAAS,gBAAgB,eAAe,OAAO;AACpE,UAAI,gBAAgB,EAAG;AACvB,YAAM,MAAM,KAAK,MAAO,OAAO,UAAU,eAAgB,GAAG;AAC5D,UAAI,MAAM,aAAa,QAAS,cAAa,UAAU;AAAA,IACzD;AAEA,WAAO,iBAAiB,UAAU,cAAc,EAAE,SAAS,KAAK,CAAC;AACjE,WAAO,MAAM,OAAO,oBAAoB,UAAU,YAAY;AAAA,EAChE,GAAG,CAAC,WAAW,CAAC;AAGhB,+BAAU,MAAM;AACd,QAAI,CAAC,eAAe,CAAC,UAAW;AAEhC,UAAM,eAAe,MAAM;AACzB,YAAM,aAAa,KAAK,OAAO,KAAK,IAAI,IAAI,aAAa,WAAW,GAAI;AACxE,YAAM,WAAW,YAAY,QAAQ,MAAM,GAAG,EAAE,OAAO,OAAO;AAE9D,YAAM,UAAU,KAAK,UAAU;AAAA,QAC7B,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,mBAAmB,SAAS,CAAC,KAAK;AAAA,QAClC,WAAW,SAAS,UAAU,IAAI,SAAS,SAAS,SAAS,CAAC,IAAI;AAAA,QAClE,YAAY,aAAa;AAAA,QACzB,UAAU;AAAA,UACR,cAAc,aAAa;AAAA,UAC3B,cAAc;AAAA,UACd,MAAM,YAAY;AAAA,QACpB;AAAA,MACF,CAAC;AAED,UAAI,UAAU,YAAY;AACxB,kBAAU,WAAW,aAAa,OAAO;AAAA,MAC3C;AAAA,IACF;AAEA,WAAO,iBAAiB,gBAAgB,YAAY;AACpD,WAAO,MAAM,OAAO,oBAAoB,gBAAgB,YAAY;AAAA,EACtE,GAAG,CAAC,aAAa,QAAQ,aAAa,SAAS,CAAC;AAEhD,SAAO;AACT;AAEA,SAASA,WAAU,KAAa,MAA+B;AAC7D,QAAM,KAAK;AAAA,IACT,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU,IAAI;AAAA,IACzB,WAAW;AAAA,EACb,CAAC,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AACnB;AAEA,SAASD,gBAAuB;AAC9B,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,MAAM;AACZ,MAAI,MAAM,eAAe,QAAQ,GAAG;AACpC,MAAI,CAAC,KAAK;AACR,UAAM,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE;AAClE,mBAAe,QAAQ,KAAK,GAAG;AAAA,EACjC;AACA,SAAO;AACT;;;AC7GM;AAtBN,SAAS,iBAAiB,OAAuB;AAC/C,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,IAAI,WAAW,MAAM,CAAC,CAAC;AAC7B,QAAM,IAAI,WAAW,MAAM,CAAC,CAAC;AAC7B,MAAI,CAAC,KAAK,CAAC,KAAK,KAAK,KAAK,KAAK,EAAG,QAAO;AACzC,SAAO,GAAG,CAAC,IAAI,CAAC;AAClB;AAEO,SAAS,SAAS,EAAE,OAAO,UAAU,GAAkB;AAC5D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,QAAQ;AACd,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,IAAI,WAAW,UAAU,EAAG,QAAO;AAE5D,QAAM,QAAQ,MAAM,SAAS;AAC7B,QAAM,cAAc,iBAAiB,MAAM,gBAAgB,MAAM;AAEjE,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,EAAE,UAAU,YAAY,OAAO,YAAY;AAAA,MAElD;AAAA,QAAC;AAAA;AAAA,UACC,KAAK,MAAM;AAAA,UACX,OAAO,EAAE,UAAU,YAAY,OAAO,GAAG,OAAO,QAAQ,QAAQ,OAAO;AAAA,UACvE,aAAY;AAAA,UACZ,SAAQ;AAAA,UACR,iBAAe;AAAA,UACf,SAAQ;AAAA;AAAA,MACV;AAAA;AAAA,EACF;AAEJ;","names":["import_react","getSessionId","sendEvent"]}
package/dist/client.mjs CHANGED
@@ -174,8 +174,45 @@ function getSessionId2() {
174
174
  }
175
175
  return sid;
176
176
  }
177
+
178
+ // src/embed-component.tsx
179
+ import { jsx } from "react/jsx-runtime";
180
+ function toCssAspectRatio(ratio) {
181
+ const parts = ratio.split(":");
182
+ if (parts.length !== 2) return "16/9";
183
+ const w = parseFloat(parts[0]);
184
+ const h = parseFloat(parts[1]);
185
+ if (!w || !h || w <= 0 || h <= 0) return "16/9";
186
+ return `${w}/${h}`;
187
+ }
188
+ function CmsEmbed({ field, className }) {
189
+ if (!field || typeof field !== "object") return null;
190
+ const embed = field;
191
+ if (!embed.url || !embed.url.startsWith("https://")) return null;
192
+ const width = embed.width || "100%";
193
+ const aspectRatio = toCssAspectRatio(embed.aspect_ratio || "16:9");
194
+ return /* @__PURE__ */ jsx(
195
+ "div",
196
+ {
197
+ className,
198
+ style: { position: "relative", width, aspectRatio },
199
+ children: /* @__PURE__ */ jsx(
200
+ "iframe",
201
+ {
202
+ src: embed.url,
203
+ style: { position: "absolute", inset: 0, width: "100%", height: "100%" },
204
+ frameBorder: "0",
205
+ sandbox: "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox",
206
+ allowFullScreen: true,
207
+ loading: "lazy"
208
+ }
209
+ )
210
+ }
211
+ );
212
+ }
177
213
  export {
178
214
  CmsAnalytics,
215
+ CmsEmbed,
179
216
  PageTracker
180
217
  };
181
218
  //# sourceMappingURL=client.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/analytics.tsx","../src/page-tracker.tsx"],"sourcesContent":["\"use client\"\n\nimport { useEffect, useRef } from \"react\"\n\ninterface CmsAnalyticsProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Content item ID (from CMS) */\n contentItemId?: string\n /** Content type slug (e.g. \"blog_posts\") */\n contentTypeSlug: string\n /** Item slug (e.g. \"my-post\") */\n itemSlug: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Drop-in analytics component for CMS content pages.\n * Tracks page views, scroll depth, and time on page.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage:\n * ```tsx\n * <CmsAnalytics\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * contentTypeSlug=\"blog_posts\"\n * itemSlug={params.slug}\n * />\n * ```\n */\nexport function CmsAnalytics({\n trackingUrl,\n apiKey,\n contentItemId,\n contentTypeSlug,\n itemSlug,\n trackScroll = true,\n trackTime = true,\n}: CmsAnalyticsProps) {\n const sentRef = useRef(false)\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Only send page view once per mount\n if (sentRef.current) return\n sentRef.current = true\n\n // Page view event\n sendEvent(trackingUrl, {\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"page_view\",\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n })\n\n // Scroll tracking\n let scrollHandler: (() => void) | null = null\n if (trackScroll) {\n scrollHandler = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n window.addEventListener(\"scroll\", scrollHandler, { passive: true })\n }\n\n // Send engagement data on page leave\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const payload = JSON.stringify({\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"engagement\",\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n },\n })\n\n // Use sendBeacon for reliable delivery on page unload\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n if (trackScroll || trackTime) {\n window.addEventListener(\"beforeunload\", handleUnload)\n }\n\n return () => {\n if (scrollHandler) window.removeEventListener(\"scroll\", scrollHandler)\n if (trackScroll || trackTime) window.removeEventListener(\"beforeunload\", handleUnload)\n }\n }, [trackingUrl, apiKey, contentItemId, contentTypeSlug, itemSlug, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {}) // fire and forget\n}\n\n/** Generate a random session ID (persists for the browser session) */\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n","\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { usePathname } from \"next/navigation\"\n\ninterface PageTrackerProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Site-wide page tracker. Add once in root layout to track all pages.\n * Automatically detects CMS content pages from URL structure.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage in src/app/layout.tsx:\n * ```tsx\n * import { PageTracker } from \"@distinctagency/cms-client/client\"\n *\n * <body>\n * {children}\n * <PageTracker\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * />\n * </body>\n * ```\n */\nexport function PageTracker({\n trackingUrl,\n apiKey,\n trackScroll = true,\n trackTime = true,\n}: PageTrackerProps) {\n const pathname = usePathname()\n const prevPathRef = useRef(\"\")\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Skip if same path (prevents double-fire on mount)\n if (pathname === prevPathRef.current) return\n prevPathRef.current = pathname\n\n // Reset engagement tracking for new page\n maxScrollRef.current = 0\n startTimeRef.current = Date.now()\n\n // Try to extract content type and item slug from URL\n // Supports patterns like /events/future-finance, /blog/my-post, etc.\n const segments = pathname.split(\"/\").filter(Boolean)\n const contentTypeSlug = segments[0] ?? null\n const itemSlug = segments.length >= 2 ? segments[segments.length - 1] : null\n\n // Fire page view\n sendEvent(trackingUrl, {\n api_key: apiKey,\n event_type: \"page_view\",\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n metadata: { path: pathname },\n })\n }, [pathname, trackingUrl, apiKey])\n\n // Scroll tracking\n useEffect(() => {\n if (!trackScroll) return\n\n const handleScroll = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n\n window.addEventListener(\"scroll\", handleScroll, { passive: true })\n return () => window.removeEventListener(\"scroll\", handleScroll)\n }, [trackScroll])\n\n // Send engagement data on page leave\n useEffect(() => {\n if (!trackScroll && !trackTime) return\n\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const segments = prevPathRef.current.split(\"/\").filter(Boolean)\n\n const payload = JSON.stringify({\n api_key: apiKey,\n event_type: \"engagement\",\n content_type_slug: segments[0] ?? null,\n item_slug: segments.length >= 2 ? segments[segments.length - 1] : null,\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n path: prevPathRef.current,\n },\n })\n\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n window.addEventListener(\"beforeunload\", handleUnload)\n return () => window.removeEventListener(\"beforeunload\", handleUnload)\n }, [trackingUrl, apiKey, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {})\n}\n\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n"],"mappings":"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","names":["useEffect","useRef","getSessionId","sendEvent"]}
1
+ {"version":3,"sources":["../src/analytics.tsx","../src/page-tracker.tsx","../src/embed-component.tsx"],"sourcesContent":["\"use client\"\n\nimport { useEffect, useRef } from \"react\"\n\ninterface CmsAnalyticsProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Content item ID (from CMS) */\n contentItemId?: string\n /** Content type slug (e.g. \"blog_posts\") */\n contentTypeSlug: string\n /** Item slug (e.g. \"my-post\") */\n itemSlug: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Drop-in analytics component for CMS content pages.\n * Tracks page views, scroll depth, and time on page.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage:\n * ```tsx\n * <CmsAnalytics\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * contentTypeSlug=\"blog_posts\"\n * itemSlug={params.slug}\n * />\n * ```\n */\nexport function CmsAnalytics({\n trackingUrl,\n apiKey,\n contentItemId,\n contentTypeSlug,\n itemSlug,\n trackScroll = true,\n trackTime = true,\n}: CmsAnalyticsProps) {\n const sentRef = useRef(false)\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Only send page view once per mount\n if (sentRef.current) return\n sentRef.current = true\n\n // Page view event\n sendEvent(trackingUrl, {\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"page_view\",\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n })\n\n // Scroll tracking\n let scrollHandler: (() => void) | null = null\n if (trackScroll) {\n scrollHandler = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n window.addEventListener(\"scroll\", scrollHandler, { passive: true })\n }\n\n // Send engagement data on page leave\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const payload = JSON.stringify({\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"engagement\",\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n },\n })\n\n // Use sendBeacon for reliable delivery on page unload\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n if (trackScroll || trackTime) {\n window.addEventListener(\"beforeunload\", handleUnload)\n }\n\n return () => {\n if (scrollHandler) window.removeEventListener(\"scroll\", scrollHandler)\n if (trackScroll || trackTime) window.removeEventListener(\"beforeunload\", handleUnload)\n }\n }, [trackingUrl, apiKey, contentItemId, contentTypeSlug, itemSlug, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {}) // fire and forget\n}\n\n/** Generate a random session ID (persists for the browser session) */\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n","\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { usePathname } from \"next/navigation\"\n\ninterface PageTrackerProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Site-wide page tracker. Add once in root layout to track all pages.\n * Automatically detects CMS content pages from URL structure.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage in src/app/layout.tsx:\n * ```tsx\n * import { PageTracker } from \"@distinctagency/cms-client/client\"\n *\n * <body>\n * {children}\n * <PageTracker\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * />\n * </body>\n * ```\n */\nexport function PageTracker({\n trackingUrl,\n apiKey,\n trackScroll = true,\n trackTime = true,\n}: PageTrackerProps) {\n const pathname = usePathname()\n const prevPathRef = useRef(\"\")\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Skip if same path (prevents double-fire on mount)\n if (pathname === prevPathRef.current) return\n prevPathRef.current = pathname\n\n // Reset engagement tracking for new page\n maxScrollRef.current = 0\n startTimeRef.current = Date.now()\n\n // Try to extract content type and item slug from URL\n // Supports patterns like /events/future-finance, /blog/my-post, etc.\n const segments = pathname.split(\"/\").filter(Boolean)\n const contentTypeSlug = segments[0] ?? null\n const itemSlug = segments.length >= 2 ? segments[segments.length - 1] : null\n\n // Fire page view\n sendEvent(trackingUrl, {\n api_key: apiKey,\n event_type: \"page_view\",\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n metadata: { path: pathname },\n })\n }, [pathname, trackingUrl, apiKey])\n\n // Scroll tracking\n useEffect(() => {\n if (!trackScroll) return\n\n const handleScroll = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n\n window.addEventListener(\"scroll\", handleScroll, { passive: true })\n return () => window.removeEventListener(\"scroll\", handleScroll)\n }, [trackScroll])\n\n // Send engagement data on page leave\n useEffect(() => {\n if (!trackScroll && !trackTime) return\n\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const segments = prevPathRef.current.split(\"/\").filter(Boolean)\n\n const payload = JSON.stringify({\n api_key: apiKey,\n event_type: \"engagement\",\n content_type_slug: segments[0] ?? null,\n item_slug: segments.length >= 2 ? segments[segments.length - 1] : null,\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n path: prevPathRef.current,\n },\n })\n\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n window.addEventListener(\"beforeunload\", handleUnload)\n return () => window.removeEventListener(\"beforeunload\", handleUnload)\n }, [trackingUrl, apiKey, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {})\n}\n\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport type { EmbedValue } from \"./types\"\n\ninterface CmsEmbedProps {\n field: unknown\n className?: string\n}\n\nfunction toCssAspectRatio(ratio: string): string {\n const parts = ratio.split(\":\")\n if (parts.length !== 2) return \"16/9\"\n const w = parseFloat(parts[0])\n const h = parseFloat(parts[1])\n if (!w || !h || w <= 0 || h <= 0) return \"16/9\"\n return `${w}/${h}`\n}\n\nexport function CmsEmbed({ field, className }: CmsEmbedProps) {\n if (!field || typeof field !== \"object\") return null\n const embed = field as EmbedValue\n if (!embed.url || !embed.url.startsWith(\"https://\")) return null\n\n const width = embed.width || \"100%\"\n const aspectRatio = toCssAspectRatio(embed.aspect_ratio || \"16:9\")\n\n return (\n <div\n className={className}\n style={{ position: \"relative\", width, aspectRatio }}\n >\n <iframe\n src={embed.url}\n style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\" }}\n frameBorder=\"0\"\n sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox\"\n allowFullScreen\n loading=\"lazy\"\n />\n </div>\n )\n}\n"],"mappings":"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","names":["useEffect","useRef","getSessionId","sendEvent"]}
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { SupabaseClient } from '@supabase/supabase-js';
2
2
 
3
3
  /** Field types supported by the CMS schema */
4
- type FieldType = "text" | "textarea" | "richtext" | "date" | "datetime" | "select" | "number" | "boolean" | "image" | "gallery" | "url" | "file" | "reference" | "computed" | "color" | "tags" | "json";
4
+ type FieldType = "text" | "textarea" | "richtext" | "date" | "datetime" | "select" | "number" | "boolean" | "image" | "gallery" | "url" | "file" | "reference" | "computed" | "color" | "tags" | "json" | "embed" | "flipbook";
5
5
  interface ImageConfig {
6
6
  max_width?: number;
7
7
  max_height?: number;
@@ -282,6 +282,30 @@ interface ReviewQueryOptions {
282
282
  /** Sort direction — defaults to 'desc' */
283
283
  orderDirection?: "asc" | "desc";
284
284
  }
285
+ interface EmbedValue {
286
+ url: string;
287
+ width: string;
288
+ aspect_ratio: string;
289
+ }
290
+ interface FlipbookPagePublic {
291
+ url: string;
292
+ thumb_url: string;
293
+ w: number;
294
+ h: number;
295
+ }
296
+ interface FlipbookTocEntryPublic {
297
+ title: string;
298
+ page: number;
299
+ }
300
+ interface FlipbookPublic {
301
+ id: string;
302
+ title: string;
303
+ page_count: number;
304
+ status: "pending_upload" | "pending" | "processing" | "ready" | "failed";
305
+ page_images: FlipbookPagePublic[];
306
+ toc: FlipbookTocEntryPublic[];
307
+ download_url: string | null;
308
+ }
285
309
 
286
310
  /**
287
311
  * Creates a CMS query client authenticated with a tenant API key.
@@ -353,6 +377,11 @@ declare function createCmsClient(supabase: SupabaseClient, options: CmsClientOpt
353
377
  * Get a form configuration by slug.
354
378
  */
355
379
  getForm(formSlug: string): Promise<Record<string, unknown> | null>;
380
+ /**
381
+ * Get a flipbook by ID, including CDN-resolved page image URLs.
382
+ * Returns null if not found or manifest not yet ready.
383
+ */
384
+ getFlipbook: (id: string) => Promise<FlipbookPublic | null>;
356
385
  /**
357
386
  * Get Google Reviews for this tenant.
358
387
  * Defaults to approved reviews ordered by most recent.
@@ -360,6 +389,12 @@ declare function createCmsClient(supabase: SupabaseClient, options: CmsClientOpt
360
389
  getReviews(options?: ReviewQueryOptions): Promise<GoogleReview[]>;
361
390
  };
362
391
 
392
+ /**
393
+ * Generate responsive iframe HTML for an embed field value.
394
+ * Returns an empty string if the value is invalid or the URL is not HTTPS.
395
+ */
396
+ declare function getEmbedHtml(value: unknown): string;
397
+
363
398
  declare function createShopClient(supabase: SupabaseClient, options: {
364
399
  apiKey: string;
365
400
  appUrl?: string;
@@ -370,6 +405,86 @@ declare function createShopClient(supabase: SupabaseClient, options: {
370
405
  createOrder(params: CreateOrderParams): Promise<CreateOrderResult>;
371
406
  };
372
407
 
408
+ type EventStatus = "draft" | "published" | "archived";
409
+ type BookingStatus = "pending" | "confirmed" | "cancelled" | "refunded";
410
+ interface CmsEvent {
411
+ id: string;
412
+ tenant_id: string;
413
+ title: string;
414
+ slug: string;
415
+ description: string | null;
416
+ short_description: string | null;
417
+ status: EventStatus;
418
+ start_at: string;
419
+ end_at: string | null;
420
+ venue_name: string | null;
421
+ venue_address: string | null;
422
+ hero_image: string | null;
423
+ gallery: string[];
424
+ tags: string[];
425
+ seo_title: string | null;
426
+ seo_description: string | null;
427
+ metadata: Record<string, unknown>;
428
+ sort_order: number;
429
+ published_at: string | null;
430
+ created_at: string;
431
+ updated_at: string;
432
+ }
433
+ interface CmsTicketTier {
434
+ id: string;
435
+ event_id: string;
436
+ tenant_id: string;
437
+ name: string;
438
+ description: string | null;
439
+ price_cents: number;
440
+ capacity: number | null;
441
+ sold_count: number;
442
+ sales_start_at: string | null;
443
+ sales_end_at: string | null;
444
+ is_active: boolean;
445
+ sort_order: number;
446
+ }
447
+ interface EventWithTiers extends CmsEvent {
448
+ tiers: CmsTicketTier[];
449
+ }
450
+ interface EventQueryOptions {
451
+ /** Only include events with start_at >= now. Default false. */
452
+ upcomingOnly?: boolean;
453
+ tag?: string;
454
+ limit?: number;
455
+ offset?: number;
456
+ sort?: "start_at" | "sort_order" | "created_at";
457
+ order?: "asc" | "desc";
458
+ }
459
+ interface CreateBookingParams {
460
+ event_slug: string;
461
+ ticket_tier_id?: string;
462
+ customer_email: string;
463
+ customer_name?: string;
464
+ customer_phone?: string;
465
+ quantity?: number;
466
+ success_url?: string;
467
+ cancel_url?: string;
468
+ metadata?: Record<string, unknown>;
469
+ }
470
+ interface CreateBookingResult {
471
+ booking_id: string;
472
+ booking_number: string;
473
+ status: "pending" | "confirmed";
474
+ /** Redirect the browser here for paid bookings. */
475
+ checkout_url?: string;
476
+ /** Present on free bookings — used for attendance QR display. */
477
+ qr_token?: string;
478
+ }
479
+ declare function createEventsClient(supabase: SupabaseClient, options: {
480
+ apiKey: string;
481
+ appUrl?: string;
482
+ }): {
483
+ getEvents: (queryOptions?: EventQueryOptions) => Promise<EventWithTiers[]>;
484
+ getEventBySlug: (slug: string) => Promise<EventWithTiers | null>;
485
+ createBooking: (params: CreateBookingParams) => Promise<CreateBookingResult>;
486
+ };
487
+
373
488
  /**
374
489
  * Image helpers for CMS content.
375
490
  *
@@ -448,4 +563,4 @@ declare const IMAGE_PRESETS: {
448
563
  };
449
564
  };
450
565
 
451
- export { type CmsClientOptions, type ContentItem, type ContentQueryOptions, type ContentType, type ContentTypeSeoConfig, type CreateOrderParams, type CreateOrderResult, type FieldDefinition, type FieldType, type GoogleReview, IMAGE_PRESETS, type ImageConfig, type ImageTransformOptions, type MediaFolder, type MediaItem, type Member, type MembershipTier, type OrderAddress, type Product, type ProductOption, type ProductQueryOptions, type ProductVariant, type Profile, type ReviewQueryOptions, type Tenant, type TenantMembership, createCmsClient, createShopClient, getSrcSet, getTransformUrl };
566
+ export { type BookingStatus, type CmsClientOptions, type CmsEvent, type CmsTicketTier, type ContentItem, type ContentQueryOptions, type ContentType, type ContentTypeSeoConfig, type CreateBookingParams, type CreateBookingResult, type CreateOrderParams, type CreateOrderResult, type EmbedValue, type EventQueryOptions, type EventStatus, type EventWithTiers, type FieldDefinition, type FieldType, type FlipbookPagePublic, type FlipbookPublic, type FlipbookTocEntryPublic, type GoogleReview, IMAGE_PRESETS, type ImageConfig, type ImageTransformOptions, type MediaFolder, type MediaItem, type Member, type MembershipTier, type OrderAddress, type Product, type ProductOption, type ProductQueryOptions, type ProductVariant, type Profile, type ReviewQueryOptions, type Tenant, type TenantMembership, createCmsClient, createEventsClient, createShopClient, getEmbedHtml, getSrcSet, getTransformUrl };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { SupabaseClient } from '@supabase/supabase-js';
2
2
 
3
3
  /** Field types supported by the CMS schema */
4
- type FieldType = "text" | "textarea" | "richtext" | "date" | "datetime" | "select" | "number" | "boolean" | "image" | "gallery" | "url" | "file" | "reference" | "computed" | "color" | "tags" | "json";
4
+ type FieldType = "text" | "textarea" | "richtext" | "date" | "datetime" | "select" | "number" | "boolean" | "image" | "gallery" | "url" | "file" | "reference" | "computed" | "color" | "tags" | "json" | "embed" | "flipbook";
5
5
  interface ImageConfig {
6
6
  max_width?: number;
7
7
  max_height?: number;
@@ -282,6 +282,30 @@ interface ReviewQueryOptions {
282
282
  /** Sort direction — defaults to 'desc' */
283
283
  orderDirection?: "asc" | "desc";
284
284
  }
285
+ interface EmbedValue {
286
+ url: string;
287
+ width: string;
288
+ aspect_ratio: string;
289
+ }
290
+ interface FlipbookPagePublic {
291
+ url: string;
292
+ thumb_url: string;
293
+ w: number;
294
+ h: number;
295
+ }
296
+ interface FlipbookTocEntryPublic {
297
+ title: string;
298
+ page: number;
299
+ }
300
+ interface FlipbookPublic {
301
+ id: string;
302
+ title: string;
303
+ page_count: number;
304
+ status: "pending_upload" | "pending" | "processing" | "ready" | "failed";
305
+ page_images: FlipbookPagePublic[];
306
+ toc: FlipbookTocEntryPublic[];
307
+ download_url: string | null;
308
+ }
285
309
 
286
310
  /**
287
311
  * Creates a CMS query client authenticated with a tenant API key.
@@ -353,6 +377,11 @@ declare function createCmsClient(supabase: SupabaseClient, options: CmsClientOpt
353
377
  * Get a form configuration by slug.
354
378
  */
355
379
  getForm(formSlug: string): Promise<Record<string, unknown> | null>;
380
+ /**
381
+ * Get a flipbook by ID, including CDN-resolved page image URLs.
382
+ * Returns null if not found or manifest not yet ready.
383
+ */
384
+ getFlipbook: (id: string) => Promise<FlipbookPublic | null>;
356
385
  /**
357
386
  * Get Google Reviews for this tenant.
358
387
  * Defaults to approved reviews ordered by most recent.
@@ -360,6 +389,12 @@ declare function createCmsClient(supabase: SupabaseClient, options: CmsClientOpt
360
389
  getReviews(options?: ReviewQueryOptions): Promise<GoogleReview[]>;
361
390
  };
362
391
 
392
+ /**
393
+ * Generate responsive iframe HTML for an embed field value.
394
+ * Returns an empty string if the value is invalid or the URL is not HTTPS.
395
+ */
396
+ declare function getEmbedHtml(value: unknown): string;
397
+
363
398
  declare function createShopClient(supabase: SupabaseClient, options: {
364
399
  apiKey: string;
365
400
  appUrl?: string;
@@ -370,6 +405,86 @@ declare function createShopClient(supabase: SupabaseClient, options: {
370
405
  createOrder(params: CreateOrderParams): Promise<CreateOrderResult>;
371
406
  };
372
407
 
408
+ type EventStatus = "draft" | "published" | "archived";
409
+ type BookingStatus = "pending" | "confirmed" | "cancelled" | "refunded";
410
+ interface CmsEvent {
411
+ id: string;
412
+ tenant_id: string;
413
+ title: string;
414
+ slug: string;
415
+ description: string | null;
416
+ short_description: string | null;
417
+ status: EventStatus;
418
+ start_at: string;
419
+ end_at: string | null;
420
+ venue_name: string | null;
421
+ venue_address: string | null;
422
+ hero_image: string | null;
423
+ gallery: string[];
424
+ tags: string[];
425
+ seo_title: string | null;
426
+ seo_description: string | null;
427
+ metadata: Record<string, unknown>;
428
+ sort_order: number;
429
+ published_at: string | null;
430
+ created_at: string;
431
+ updated_at: string;
432
+ }
433
+ interface CmsTicketTier {
434
+ id: string;
435
+ event_id: string;
436
+ tenant_id: string;
437
+ name: string;
438
+ description: string | null;
439
+ price_cents: number;
440
+ capacity: number | null;
441
+ sold_count: number;
442
+ sales_start_at: string | null;
443
+ sales_end_at: string | null;
444
+ is_active: boolean;
445
+ sort_order: number;
446
+ }
447
+ interface EventWithTiers extends CmsEvent {
448
+ tiers: CmsTicketTier[];
449
+ }
450
+ interface EventQueryOptions {
451
+ /** Only include events with start_at >= now. Default false. */
452
+ upcomingOnly?: boolean;
453
+ tag?: string;
454
+ limit?: number;
455
+ offset?: number;
456
+ sort?: "start_at" | "sort_order" | "created_at";
457
+ order?: "asc" | "desc";
458
+ }
459
+ interface CreateBookingParams {
460
+ event_slug: string;
461
+ ticket_tier_id?: string;
462
+ customer_email: string;
463
+ customer_name?: string;
464
+ customer_phone?: string;
465
+ quantity?: number;
466
+ success_url?: string;
467
+ cancel_url?: string;
468
+ metadata?: Record<string, unknown>;
469
+ }
470
+ interface CreateBookingResult {
471
+ booking_id: string;
472
+ booking_number: string;
473
+ status: "pending" | "confirmed";
474
+ /** Redirect the browser here for paid bookings. */
475
+ checkout_url?: string;
476
+ /** Present on free bookings — used for attendance QR display. */
477
+ qr_token?: string;
478
+ }
479
+ declare function createEventsClient(supabase: SupabaseClient, options: {
480
+ apiKey: string;
481
+ appUrl?: string;
482
+ }): {
483
+ getEvents: (queryOptions?: EventQueryOptions) => Promise<EventWithTiers[]>;
484
+ getEventBySlug: (slug: string) => Promise<EventWithTiers | null>;
485
+ createBooking: (params: CreateBookingParams) => Promise<CreateBookingResult>;
486
+ };
487
+
373
488
  /**
374
489
  * Image helpers for CMS content.
375
490
  *
@@ -448,4 +563,4 @@ declare const IMAGE_PRESETS: {
448
563
  };
449
564
  };
450
565
 
451
- export { type CmsClientOptions, type ContentItem, type ContentQueryOptions, type ContentType, type ContentTypeSeoConfig, type CreateOrderParams, type CreateOrderResult, type FieldDefinition, type FieldType, type GoogleReview, IMAGE_PRESETS, type ImageConfig, type ImageTransformOptions, type MediaFolder, type MediaItem, type Member, type MembershipTier, type OrderAddress, type Product, type ProductOption, type ProductQueryOptions, type ProductVariant, type Profile, type ReviewQueryOptions, type Tenant, type TenantMembership, createCmsClient, createShopClient, getSrcSet, getTransformUrl };
566
+ export { type BookingStatus, type CmsClientOptions, type CmsEvent, type CmsTicketTier, type ContentItem, type ContentQueryOptions, type ContentType, type ContentTypeSeoConfig, type CreateBookingParams, type CreateBookingResult, type CreateOrderParams, type CreateOrderResult, type EmbedValue, type EventQueryOptions, type EventStatus, type EventWithTiers, type FieldDefinition, type FieldType, type FlipbookPagePublic, type FlipbookPublic, type FlipbookTocEntryPublic, type GoogleReview, IMAGE_PRESETS, type ImageConfig, type ImageTransformOptions, type MediaFolder, type MediaItem, type Member, type MembershipTier, type OrderAddress, type Product, type ProductOption, type ProductQueryOptions, type ProductVariant, type Profile, type ReviewQueryOptions, type Tenant, type TenantMembership, createCmsClient, createEventsClient, createShopClient, getEmbedHtml, getSrcSet, getTransformUrl };
package/dist/index.js CHANGED
@@ -22,7 +22,9 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  IMAGE_PRESETS: () => IMAGE_PRESETS,
24
24
  createCmsClient: () => createCmsClient,
25
+ createEventsClient: () => createEventsClient,
25
26
  createShopClient: () => createShopClient,
27
+ getEmbedHtml: () => getEmbedHtml,
26
28
  getSrcSet: () => getSrcSet,
27
29
  getTransformUrl: () => getTransformUrl
28
30
  });
@@ -74,6 +76,39 @@ function createCmsClient(supabase, options) {
74
76
  memberTierId: member.membership_tier_id
75
77
  };
76
78
  }
79
+ async function getFlipbook(id) {
80
+ const { data, error } = await client.from("flipbooks").select("id, title, page_count, status, manifest, download_enabled, tenant_id").eq("id", id).single();
81
+ if (error || !data) return null;
82
+ const manifest = data.manifest;
83
+ if (!manifest) {
84
+ return {
85
+ id: data.id,
86
+ title: data.title,
87
+ page_count: data.page_count ?? 0,
88
+ status: data.status,
89
+ page_images: [],
90
+ toc: [],
91
+ download_url: null
92
+ };
93
+ }
94
+ const proc = globalThis.process;
95
+ const cdnBase = proc?.env?.NEXT_PUBLIC_R2_PUBLIC_URL ?? "https://cdn.distinctstudio.co.nz";
96
+ const prefix = `flipbooks/${data.tenant_id}/${data.id}/`;
97
+ return {
98
+ id: data.id,
99
+ title: data.title,
100
+ page_count: data.page_count ?? manifest.pages.length,
101
+ status: data.status,
102
+ page_images: manifest.pages.map((p) => ({
103
+ url: `${cdnBase}/${prefix}${p.image}`,
104
+ thumb_url: `${cdnBase}/${prefix}${p.thumb}`,
105
+ w: p.w,
106
+ h: p.h
107
+ })),
108
+ toc: manifest.toc,
109
+ download_url: data.download_enabled ? `${options.appUrl ?? ""}/api/flipbooks/${data.id}/download` : null
110
+ };
111
+ }
77
112
  return {
78
113
  /**
79
114
  * List content items for a content type.
@@ -220,6 +255,11 @@ function createCmsClient(supabase, options) {
220
255
  const { data } = await client.from("forms").select("id, name, slug, description, is_active").eq("tenant_id", tenantId).eq("slug", formSlug).single();
221
256
  return data;
222
257
  },
258
+ /**
259
+ * Get a flipbook by ID, including CDN-resolved page image URLs.
260
+ * Returns null if not found or manifest not yet ready.
261
+ */
262
+ getFlipbook,
223
263
  /**
224
264
  * Get Google Reviews for this tenant.
225
265
  * Defaults to approved reviews ordered by most recent.
@@ -262,6 +302,24 @@ function withApiKey(supabase, apiKey) {
262
302
  });
263
303
  }
264
304
 
305
+ // src/embed.ts
306
+ function toCssAspectRatio(ratio) {
307
+ const parts = ratio.split(":");
308
+ if (parts.length !== 2) return "16/9";
309
+ const w = parseFloat(parts[0]);
310
+ const h = parseFloat(parts[1]);
311
+ if (!w || !h || w <= 0 || h <= 0) return "16/9";
312
+ return `${w}/${h}`;
313
+ }
314
+ function getEmbedHtml(value) {
315
+ if (!value || typeof value !== "object") return "";
316
+ const embed = value;
317
+ if (!embed.url || !embed.url.startsWith("https://")) return "";
318
+ const width = embed.width || "100%";
319
+ const aspectRatio = toCssAspectRatio(embed.aspect_ratio || "16:9");
320
+ return `<div style="position:relative;width:${width};aspect-ratio:${aspectRatio}"><iframe src="${embed.url}" style="position:absolute;inset:0;width:100%;height:100%" frameborder="0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" allowfullscreen loading="lazy"></iframe></div>`;
321
+ }
322
+
265
323
  // src/shop.ts
266
324
  function createShopClient(supabase, options) {
267
325
  const { apiKey, appUrl } = options;
@@ -308,6 +366,51 @@ function createShopClient(supabase, options) {
308
366
  };
309
367
  }
310
368
 
369
+ // src/events.ts
370
+ function createEventsClient(supabase, options) {
371
+ const { apiKey, appUrl } = options;
372
+ async function getEvents(queryOptions) {
373
+ let query = supabase.from("events").select("*, tiers:ticket_tiers(*)").eq("status", "published").order(queryOptions?.sort ?? "start_at", {
374
+ ascending: (queryOptions?.order ?? "asc") === "asc"
375
+ });
376
+ if (queryOptions?.upcomingOnly) {
377
+ query = query.gte("start_at", (/* @__PURE__ */ new Date()).toISOString());
378
+ }
379
+ if (queryOptions?.tag) {
380
+ query = query.contains("tags", [queryOptions.tag]);
381
+ }
382
+ if (queryOptions?.limit) {
383
+ query = query.limit(queryOptions.limit);
384
+ }
385
+ if (queryOptions?.offset) {
386
+ query = query.range(
387
+ queryOptions.offset,
388
+ queryOptions.offset + (queryOptions.limit ?? 50) - 1
389
+ );
390
+ }
391
+ const { data } = await query;
392
+ return data ?? [];
393
+ }
394
+ async function getEventBySlug(slug) {
395
+ const { data } = await supabase.from("events").select("*, tiers:ticket_tiers(*)").eq("slug", slug).eq("status", "published").single();
396
+ return data ?? null;
397
+ }
398
+ async function createBooking(params) {
399
+ const baseUrl = appUrl ?? "";
400
+ const res = await fetch(`${baseUrl}/api/bookings/create`, {
401
+ method: "POST",
402
+ headers: { "Content-Type": "application/json" },
403
+ body: JSON.stringify({ api_key: apiKey, ...params })
404
+ });
405
+ if (!res.ok) {
406
+ const err = await res.json().catch(() => ({ error: "Booking creation failed" }));
407
+ throw new Error(err.error ?? "Booking creation failed");
408
+ }
409
+ return res.json();
410
+ }
411
+ return { getEvents, getEventBySlug, createBooking };
412
+ }
413
+
311
414
  // src/cdn.ts
312
415
  function getTransformUrl(originalUrl, _options = {}) {
313
416
  return originalUrl;
@@ -327,7 +430,9 @@ var IMAGE_PRESETS = {
327
430
  0 && (module.exports = {
328
431
  IMAGE_PRESETS,
329
432
  createCmsClient,
433
+ createEventsClient,
330
434
  createShopClient,
435
+ getEmbedHtml,
331
436
  getSrcSet,
332
437
  getTransformUrl
333
438
  });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/queries.ts","../src/shop.ts","../src/cdn.ts"],"sourcesContent":["// Server-safe exports — no React, no \"use client\"\nexport { createCmsClient } from \"./queries\"\nexport { createShopClient } from \"./shop\"\nexport { getTransformUrl, getSrcSet, IMAGE_PRESETS } from \"./cdn\"\nexport type { ImageTransformOptions } from \"./cdn\"\nexport type {\n FieldType,\n FieldDefinition,\n Tenant,\n Profile,\n ContentType,\n ContentItem,\n MediaItem,\n MediaFolder,\n ContentQueryOptions,\n CmsClientOptions,\n TenantMembership,\n ImageConfig,\n ContentTypeSeoConfig,\n Product,\n ProductVariant,\n ProductOption,\n OrderAddress,\n CreateOrderParams,\n CreateOrderResult,\n ProductQueryOptions,\n MembershipTier,\n Member,\n GoogleReview,\n ReviewQueryOptions,\n} from \"./types\"\n","import { createClient as createSupabaseClient } from \"@supabase/supabase-js\"\nimport type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type {\n CmsClientOptions,\n ContentItem,\n ContentQueryOptions,\n ContentType,\n GoogleReview,\n ReviewQueryOptions,\n} from \"./types\"\n\n/**\n * Creates a CMS query client authenticated with a tenant API key.\n *\n * Usage:\n * ```ts\n * const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const events = await cms.getContentItems('events', { status: 'published' })\n * ```\n *\n * The API key is sent as an `x-cms-api-key` header on every request.\n * Supabase RLS policies validate it against the tenant's stored key.\n */\nexport function createCmsClient(\n supabase: SupabaseClient,\n options: CmsClientOptions\n) {\n const { apiKey } = options\n\n // Create a new Supabase client with the API key header injected globally\n const client = withApiKey(supabase, apiKey) as SupabaseClient\n\n /** Resolve the tenant ID from the API key (cached per request) */\n let tenantIdCache: string | null = null\n\n async function getTenantId(): Promise<string> {\n if (tenantIdCache) return tenantIdCache\n\n const { data, error } = await client\n .from(\"tenants\")\n .select(\"id\")\n .single()\n\n if (error || !data) {\n throw new Error(\n \"Invalid CMS API key — no tenant found. Check your apiKey.\"\n )\n }\n\n tenantIdCache = data.id\n return data.id\n }\n\n /** Resolve a content type from its slug (cached) */\n const contentTypeCache = new Map<string, ContentType>()\n\n async function getContentTypeBySlug(contentTypeSlug: string): Promise<ContentType> {\n if (contentTypeCache.has(contentTypeSlug)) return contentTypeCache.get(contentTypeSlug)!\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n const ct = data as ContentType\n contentTypeCache.set(contentTypeSlug, ct)\n return ct\n }\n\n async function getContentTypeId(contentTypeSlug: string): Promise<string> {\n const ct = await getContentTypeBySlug(contentTypeSlug)\n return ct.id\n }\n\n /** Check if a member token has access to a gated content type */\n async function checkMemberAccess(\n contentType: ContentType,\n memberToken?: string\n ): Promise<{ allowed: boolean; memberTierId: string | null }> {\n const requiredTierId = contentType.required_membership_tier_id\n if (!requiredTierId) return { allowed: true, memberTierId: null } // Public\n\n if (!memberToken) return { allowed: false, memberTierId: null } // Gated but no token\n\n // Verify the member's token and check their tier\n const { data: session } = await client\n .from(\"member_sessions\")\n .select(\"member_id\")\n .eq(\"token_hash\", memberToken) // Note: caller should hash the token\n .single()\n\n if (!session) return { allowed: false, memberTierId: null }\n\n const { data: member } = await client\n .from(\"members\")\n .select(\"membership_tier_id, status\")\n .eq(\"id\", session.member_id)\n .single()\n\n if (!member || member.status !== \"active\") return { allowed: false, memberTierId: null }\n\n // Check if the member's tier matches (or exceeds) the required tier\n // For now: exact match or the member has the required tier\n return {\n allowed: member.membership_tier_id === requiredTierId,\n memberTierId: member.membership_tier_id as string | null,\n }\n }\n\n return {\n /**\n * List content items for a content type.\n * Defaults to published items ordered by most recent.\n */\n async getContentItems(\n contentTypeSlug: string,\n options: ContentQueryOptions = {}\n ): Promise<(ContentItem & { locked?: boolean })[]> {\n const contentType = await getContentTypeBySlug(contentTypeSlug)\n\n const {\n status = \"published\",\n orderBy = \"published_at\",\n orderDirection = \"desc\",\n limit = 100,\n offset = 0,\n memberToken,\n } = options\n\n // Check membership access\n const access = await checkMemberAccess(contentType, memberToken)\n\n // If gated and no access, check gating mode\n if (!access.allowed && contentType.required_membership_tier_id) {\n // Get tenant settings for gating mode\n const tenantId = await getTenantId()\n const { data: settings } = await client\n .from(\"tenant_settings\")\n .select(\"membership_gating_mode\")\n .eq(\"tenant_id\", tenantId)\n .single()\n\n const mode = (settings?.membership_gating_mode as string) ?? \"teaser\"\n\n if (mode === \"hide\") {\n return [] // Hide: return nothing\n }\n\n // Teaser mode: return items with locked flag, no body/data\n let query = client\n .from(\"content_items\")\n .select(\"id, title, slug, status, excerpt, seo_title, seo_description, featured_image, published_at, created_at, updated_at\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) query = query.eq(\"status\", status)\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n if (error) throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n\n return (data ?? []).map((item) => ({\n ...item,\n data: {},\n locked: true,\n })) as (ContentItem & { locked: boolean })[]\n }\n\n // Full access\n let query = client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n }\n\n return (data ?? []) as ContentItem[]\n },\n\n /**\n * Get a single content item by its slug.\n * Returns null if not found.\n */\n async getContentItemBySlug(\n contentTypeSlug: string,\n itemSlug: string\n ): Promise<ContentItem | null> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"slug\", itemSlug)\n .single()\n\n if (error) {\n if (error.code === \"PGRST116\") return null // not found\n throw new Error(\n `Failed to fetch ${contentTypeSlug}/${itemSlug}: ${error.message}`\n )\n }\n\n return data as ContentItem\n },\n\n /**\n * Get a content type definition (including its field schema).\n */\n async getContentType(contentTypeSlug: string): Promise<ContentType> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n return data as ContentType\n },\n\n /**\n * Get all slugs for a content type (for generateStaticParams).\n */\n async getAllSlugs(\n contentTypeSlug: string\n ): Promise<{ slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"slug\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"status\", \"published\")\n\n if (error) {\n throw new Error(\n `Failed to fetch slugs for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { slug: string }[]\n },\n\n /**\n * List all content types for this tenant.\n */\n async getContentTypes(): Promise<ContentType[]> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .order(\"name\")\n\n if (error) {\n throw new Error(`Failed to fetch content types: ${error.message}`)\n }\n\n return (data ?? []) as ContentType[]\n },\n\n /**\n * Get all slug redirects for a content type.\n * Use in next.config.js redirects() or middleware for 301s.\n * Returns: [{ old_slug, new_slug }, ...]\n */\n async getRedirects(\n contentTypeSlug: string\n ): Promise<{ old_slug: string; new_slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"slug_redirects\")\n .select(\"old_slug, new_slug\")\n .eq(\"content_type_id\", contentTypeId)\n\n if (error) {\n throw new Error(\n `Failed to fetch redirects for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { old_slug: string; new_slug: string }[]\n },\n\n /**\n * Get all custom path redirects for this tenant.\n * Use alongside getRedirects() in next.config.ts for full redirect coverage.\n */\n async getCustomRedirects(): Promise<\n { source_path: string; destination_path: string; permanent: boolean }[]\n > {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"custom_redirects\")\n .select(\"source_path, destination_path, permanent\")\n .eq(\"tenant_id\", tenantId)\n\n if (error) {\n throw new Error(`Failed to fetch custom redirects: ${error.message}`)\n }\n\n return (data ?? []) as {\n source_path: string\n destination_path: string\n permanent: boolean\n }[]\n },\n\n /**\n * Get form submissions for a specific form.\n * Useful for displaying testimonials, reviews, etc.\n */\n async getFormSubmissions(\n formSlug: string,\n options: { status?: string; limit?: number; offset?: number } = {}\n ): Promise<Record<string, unknown>[]> {\n const tenantId = await getTenantId()\n\n const { data: form } = await client\n .from(\"forms\")\n .select(\"id\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n if (!form) return []\n\n const { limit = 50, offset = 0, status } = options\n\n let query = client\n .from(\"form_submissions\")\n .select(\"*\")\n .eq(\"form_id\", form.id)\n .eq(\"is_spam\", false)\n .order(\"created_at\", { ascending: false })\n .range(offset, offset + limit - 1)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n const { data } = await query\n return (data ?? []) as Record<string, unknown>[]\n },\n\n /**\n * Get a form configuration by slug.\n */\n async getForm(\n formSlug: string\n ): Promise<Record<string, unknown> | null> {\n const tenantId = await getTenantId()\n\n const { data } = await client\n .from(\"forms\")\n .select(\"id, name, slug, description, is_active\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n return data as Record<string, unknown> | null\n },\n\n /**\n * Get Google Reviews for this tenant.\n * Defaults to approved reviews ordered by most recent.\n */\n async getReviews(\n options: ReviewQueryOptions = {}\n ): Promise<GoogleReview[]> {\n const tenantId = await getTenantId()\n\n const {\n status = \"approved\",\n minRating,\n limit = 50,\n offset = 0,\n orderBy = \"review_timestamp\",\n orderDirection = \"desc\",\n } = options\n\n let query = client\n .from(\"google_reviews\")\n .select(\"id, author_name, author_photo_url, rating, text, review_timestamp\")\n .eq(\"tenant_id\", tenantId)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n if (minRating) {\n query = query.gte(\"rating\", minRating)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch reviews: ${error.message}`)\n }\n\n return (data ?? []) as GoogleReview[]\n },\n }\n}\n\n/**\n * Creates a new Supabase client that includes the `x-cms-api-key`\n * header in every request, using the same URL and key as the original.\n */\nfunction withApiKey(supabase: SupabaseClient, apiKey: string) {\n // Extract URL and key from the existing client\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n return createSupabaseClient(supabaseUrl, supabaseKey, {\n global: {\n headers: {\n \"x-cms-api-key\": apiKey,\n },\n },\n })\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type { Product, ProductQueryOptions, CreateOrderParams, CreateOrderResult } from \"./types\"\n\nexport function createShopClient(supabase: SupabaseClient, options: { apiKey: string; appUrl?: string }) {\n const { apiKey, appUrl } = options\n\n return {\n async getProducts(queryOptions?: ProductQueryOptions): Promise<Product[]> {\n let query = supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"sort_order\", { ascending: (queryOptions?.order ?? \"asc\") === \"asc\" })\n\n if (queryOptions?.category) {\n query = query.eq(\"category\", queryOptions.category)\n }\n if (queryOptions?.tags?.length) {\n query = query.overlaps(\"tags\", queryOptions.tags)\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(queryOptions.offset, queryOptions.offset + (queryOptions.limit ?? 50) - 1)\n }\n\n const { data } = await query\n return (data ?? []) as Product[]\n },\n\n async getProductBySlug(slug: string): Promise<Product | null> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as Product) ?? null\n },\n\n async getProductCategories(): Promise<string[]> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"category\")\n .eq(\"status\", \"published\")\n .not(\"category\", \"is\", null)\n const categories = [...new Set((data ?? []).map((d: { category: string }) => d.category))]\n return categories.sort()\n },\n\n async createOrder(params: CreateOrderParams): Promise<CreateOrderResult> {\n // This must go through the API (not direct Supabase) because\n // order creation requires server-side Stripe PaymentIntent creation\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/orders/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res.json().catch(() => ({ error: \"Order creation failed\" }))\n throw new Error(err.error ?? \"Order creation failed\")\n }\n return res.json()\n },\n }\n}\n","/**\n * Image helpers for CMS content.\n *\n * Images are already optimised (WebP, max 2400px) on upload.\n * No server-side transforms needed — serve originals directly.\n *\n * These functions are kept for backward compatibility but no longer\n * call Supabase image transformation endpoints.\n */\n\nexport interface ImageTransformOptions {\n width?: number\n height?: number\n quality?: number\n resize?: \"contain\" | \"cover\" | \"fill\"\n format?: \"origin\"\n}\n\n/**\n * Returns the image URL directly.\n *\n * Previously this converted URLs to use Supabase's /render/image/\n * transform endpoint, but images are now pre-optimised on upload\n * (WebP, max 2400px, quality 82) so transforms are unnecessary.\n *\n * The function is kept for backward compatibility — existing code\n * that calls getTransformUrl() will continue to work without changes.\n */\nexport function getTransformUrl(\n originalUrl: string,\n _options: ImageTransformOptions = {}\n): string {\n return originalUrl\n}\n\n/**\n * Returns a simple srcSet using the original image.\n *\n * Previously generated multiple transformed widths, but since images\n * are now pre-optimised WebP, a single source is sufficient.\n * Modern browsers handle responsive display efficiently with a\n * well-sized WebP source.\n */\nexport function getSrcSet(\n originalUrl: string,\n _widths: number[] = [],\n _quality = 80\n): string {\n return `${originalUrl} 2400w`\n}\n\n/**\n * Common image size presets — kept for backward compatibility.\n * Since images are pre-optimised, these are informational only.\n */\nexport const IMAGE_PRESETS = {\n thumbnail: { width: 150, height: 150, resize: \"cover\" as const, quality: 70 },\n card: { width: 400, height: 300, resize: \"cover\" as const, quality: 80 },\n hero: { width: 1200, height: 630, resize: \"cover\" as const, quality: 85 },\n og: { width: 1200, height: 630, resize: \"cover\" as const, quality: 90 },\n avatar: { width: 80, height: 80, resize: \"cover\" as const, quality: 75 },\n full: { width: 1920, resize: \"contain\" as const, quality: 85 },\n} as const\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,yBAAqD;AAuB9C,SAAS,gBACd,UACA,SACA;AACA,QAAM,EAAE,OAAO,IAAI;AAGnB,QAAM,SAAS,WAAW,UAAU,MAAM;AAG1C,MAAI,gBAA+B;AAEnC,iBAAe,cAA+B;AAC5C,QAAI,cAAe,QAAO;AAE1B,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAC3B,KAAK,SAAS,EACd,OAAO,IAAI,EACX,OAAO;AAEV,QAAI,SAAS,CAAC,MAAM;AAClB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,oBAAgB,KAAK;AACrB,WAAO,KAAK;AAAA,EACd;AAGA,QAAM,mBAAmB,oBAAI,IAAyB;AAEtD,iBAAe,qBAAqB,iBAA+C;AACjF,QAAI,iBAAiB,IAAI,eAAe,EAAG,QAAO,iBAAiB,IAAI,eAAe;AACtF,UAAM,WAAW,MAAM,YAAY;AAEnC,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAC3B,KAAK,eAAe,EACpB,OAAO,GAAG,EACV,GAAG,aAAa,QAAQ,EACxB,GAAG,QAAQ,eAAe,EAC1B,OAAO;AAEV,QAAI,SAAS,CAAC,MAAM;AAClB,YAAM,IAAI,MAAM,2BAA2B,eAAe,EAAE;AAAA,IAC9D;AAEA,UAAM,KAAK;AACX,qBAAiB,IAAI,iBAAiB,EAAE;AACxC,WAAO;AAAA,EACT;AAEA,iBAAe,iBAAiB,iBAA0C;AACxE,UAAM,KAAK,MAAM,qBAAqB,eAAe;AACrD,WAAO,GAAG;AAAA,EACZ;AAGA,iBAAe,kBACb,aACA,aAC4D;AAC5D,UAAM,iBAAiB,YAAY;AACnC,QAAI,CAAC,eAAgB,QAAO,EAAE,SAAS,MAAM,cAAc,KAAK;AAEhE,QAAI,CAAC,YAAa,QAAO,EAAE,SAAS,OAAO,cAAc,KAAK;AAG9D,UAAM,EAAE,MAAM,QAAQ,IAAI,MAAM,OAC7B,KAAK,iBAAiB,EACtB,OAAO,WAAW,EAClB,GAAG,cAAc,WAAW,EAC5B,OAAO;AAEV,QAAI,CAAC,QAAS,QAAO,EAAE,SAAS,OAAO,cAAc,KAAK;AAE1D,UAAM,EAAE,MAAM,OAAO,IAAI,MAAM,OAC5B,KAAK,SAAS,EACd,OAAO,4BAA4B,EACnC,GAAG,MAAM,QAAQ,SAAS,EAC1B,OAAO;AAEV,QAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO,EAAE,SAAS,OAAO,cAAc,KAAK;AAIvF,WAAO;AAAA,MACL,SAAS,OAAO,uBAAuB;AAAA,MACvC,cAAc,OAAO;AAAA,IACvB;AAAA,EACF;AAEA,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAKL,MAAM,gBACJ,iBACAA,WAA+B,CAAC,GACiB;AACjD,YAAM,cAAc,MAAM,qBAAqB,eAAe;AAE9D,YAAM;AAAA,QACJ,SAAS;AAAA,QACT,UAAU;AAAA,QACV,iBAAiB;AAAA,QACjB,QAAQ;AAAA,QACR,SAAS;AAAA,QACT;AAAA,MACF,IAAIA;AAGJ,YAAM,SAAS,MAAM,kBAAkB,aAAa,WAAW;AAG/D,UAAI,CAAC,OAAO,WAAW,YAAY,6BAA6B;AAE9D,cAAM,WAAW,MAAM,YAAY;AACnC,cAAM,EAAE,MAAM,SAAS,IAAI,MAAM,OAC9B,KAAK,iBAAiB,EACtB,OAAO,wBAAwB,EAC/B,GAAG,aAAa,QAAQ,EACxB,OAAO;AAEV,cAAM,OAAQ,UAAU,0BAAqC;AAE7D,YAAI,SAAS,QAAQ;AACnB,iBAAO,CAAC;AAAA,QACV;AAGA,YAAIC,SAAQ,OACT,KAAK,eAAe,EACpB,OAAO,oHAAoH,EAC3H,GAAG,mBAAmB,YAAY,EAAE;AAEvC,YAAI,OAAQ,CAAAA,SAAQA,OAAM,GAAG,UAAU,MAAM;AAC7C,QAAAA,SAAQA,OACL,MAAM,SAAS,EAAE,WAAW,mBAAmB,MAAM,CAAC,EACtD,MAAM,QAAQ,SAAS,QAAQ,CAAC;AAEnC,cAAM,EAAE,MAAAC,OAAM,OAAAC,OAAM,IAAI,MAAMF;AAC9B,YAAIE,OAAO,OAAM,IAAI,MAAM,mBAAmB,eAAe,KAAKA,OAAM,OAAO,EAAE;AAEjF,gBAAQD,SAAQ,CAAC,GAAG,IAAI,CAAC,UAAU;AAAA,UACjC,GAAG;AAAA,UACH,MAAM,CAAC;AAAA,UACP,QAAQ;AAAA,QACV,EAAE;AAAA,MACJ;AAGA,UAAI,QAAQ,OACT,KAAK,eAAe,EACpB,OAAO,GAAG,EACV,GAAG,mBAAmB,YAAY,EAAE;AAEvC,UAAI,QAAQ;AACV,gBAAQ,MAAM,GAAG,UAAU,MAAM;AAAA,MACnC;AAEA,cAAQ,MACL,MAAM,SAAS,EAAE,WAAW,mBAAmB,MAAM,CAAC,EACtD,MAAM,QAAQ,SAAS,QAAQ,CAAC;AAEnC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM;AAE9B,UAAI,OAAO;AACT,cAAM,IAAI,MAAM,mBAAmB,eAAe,KAAK,MAAM,OAAO,EAAE;AAAA,MACxE;AAEA,aAAQ,QAAQ,CAAC;AAAA,IACnB;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM,qBACJ,iBACA,UAC6B;AAC7B,YAAM,gBAAgB,MAAM,iBAAiB,eAAe;AAE5D,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAC3B,KAAK,eAAe,EACpB,OAAO,GAAG,EACV,GAAG,mBAAmB,aAAa,EACnC,GAAG,QAAQ,QAAQ,EACnB,OAAO;AAEV,UAAI,OAAO;AACT,YAAI,MAAM,SAAS,WAAY,QAAO;AACtC,cAAM,IAAI;AAAA,UACR,mBAAmB,eAAe,IAAI,QAAQ,KAAK,MAAM,OAAO;AAAA,QAClE;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA;AAAA;AAAA;AAAA,IAKA,MAAM,eAAe,iBAA+C;AAClE,YAAM,WAAW,MAAM,YAAY;AAEnC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAC3B,KAAK,eAAe,EACpB,OAAO,GAAG,EACV,GAAG,aAAa,QAAQ,EACxB,GAAG,QAAQ,eAAe,EAC1B,OAAO;AAEV,UAAI,SAAS,CAAC,MAAM;AAClB,cAAM,IAAI,MAAM,2BAA2B,eAAe,EAAE;AAAA,MAC9D;AAEA,aAAO;AAAA,IACT;AAAA;AAAA;AAAA;AAAA,IAKA,MAAM,YACJ,iBAC6B;AAC7B,YAAM,gBAAgB,MAAM,iBAAiB,eAAe;AAE5D,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAC3B,KAAK,eAAe,EACpB,OAAO,MAAM,EACb,GAAG,mBAAmB,aAAa,EACnC,GAAG,UAAU,WAAW;AAE3B,UAAI,OAAO;AACT,cAAM,IAAI;AAAA,UACR,6BAA6B,eAAe,KAAK,MAAM,OAAO;AAAA,QAChE;AAAA,MACF;AAEA,aAAQ,QAAQ,CAAC;AAAA,IACnB;AAAA;AAAA;AAAA;AAAA,IAKA,MAAM,kBAA0C;AAC9C,YAAM,WAAW,MAAM,YAAY;AAEnC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAC3B,KAAK,eAAe,EACpB,OAAO,GAAG,EACV,GAAG,aAAa,QAAQ,EACxB,MAAM,MAAM;AAEf,UAAI,OAAO;AACT,cAAM,IAAI,MAAM,kCAAkC,MAAM,OAAO,EAAE;AAAA,MACnE;AAEA,aAAQ,QAAQ,CAAC;AAAA,IACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,MAAM,aACJ,iBACmD;AACnD,YAAM,gBAAgB,MAAM,iBAAiB,eAAe;AAE5D,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAC3B,KAAK,gBAAgB,EACrB,OAAO,oBAAoB,EAC3B,GAAG,mBAAmB,aAAa;AAEtC,UAAI,OAAO;AACT,cAAM,IAAI;AAAA,UACR,iCAAiC,eAAe,KAAK,MAAM,OAAO;AAAA,QACpE;AAAA,MACF;AAEA,aAAQ,QAAQ,CAAC;AAAA,IACnB;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM,qBAEJ;AACA,YAAM,WAAW,MAAM,YAAY;AAEnC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAC3B,KAAK,kBAAkB,EACvB,OAAO,0CAA0C,EACjD,GAAG,aAAa,QAAQ;AAE3B,UAAI,OAAO;AACT,cAAM,IAAI,MAAM,qCAAqC,MAAM,OAAO,EAAE;AAAA,MACtE;AAEA,aAAQ,QAAQ,CAAC;AAAA,IAKnB;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM,mBACJ,UACAF,WAAgE,CAAC,GAC7B;AACpC,YAAM,WAAW,MAAM,YAAY;AAEnC,YAAM,EAAE,MAAM,KAAK,IAAI,MAAM,OAC1B,KAAK,OAAO,EACZ,OAAO,IAAI,EACX,GAAG,aAAa,QAAQ,EACxB,GAAG,QAAQ,QAAQ,EACnB,OAAO;AAEV,UAAI,CAAC,KAAM,QAAO,CAAC;AAEnB,YAAM,EAAE,QAAQ,IAAI,SAAS,GAAG,OAAO,IAAIA;AAE3C,UAAI,QAAQ,OACT,KAAK,kBAAkB,EACvB,OAAO,GAAG,EACV,GAAG,WAAW,KAAK,EAAE,EACrB,GAAG,WAAW,KAAK,EACnB,MAAM,cAAc,EAAE,WAAW,MAAM,CAAC,EACxC,MAAM,QAAQ,SAAS,QAAQ,CAAC;AAEnC,UAAI,QAAQ;AACV,gBAAQ,MAAM,GAAG,UAAU,MAAM;AAAA,MACnC;AAEA,YAAM,EAAE,KAAK,IAAI,MAAM;AACvB,aAAQ,QAAQ,CAAC;AAAA,IACnB;AAAA;AAAA;AAAA;AAAA,IAKA,MAAM,QACJ,UACyC;AACzC,YAAM,WAAW,MAAM,YAAY;AAEnC,YAAM,EAAE,KAAK,IAAI,MAAM,OACpB,KAAK,OAAO,EACZ,OAAO,wCAAwC,EAC/C,GAAG,aAAa,QAAQ,EACxB,GAAG,QAAQ,QAAQ,EACnB,OAAO;AAEV,aAAO;AAAA,IACT;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM,WACJA,WAA8B,CAAC,GACN;AACzB,YAAM,WAAW,MAAM,YAAY;AAEnC,YAAM;AAAA,QACJ,SAAS;AAAA,QACT;AAAA,QACA,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,UAAU;AAAA,QACV,iBAAiB;AAAA,MACnB,IAAIA;AAEJ,UAAI,QAAQ,OACT,KAAK,gBAAgB,EACrB,OAAO,mEAAmE,EAC1E,GAAG,aAAa,QAAQ;AAE3B,UAAI,QAAQ;AACV,gBAAQ,MAAM,GAAG,UAAU,MAAM;AAAA,MACnC;AAEA,UAAI,WAAW;AACb,gBAAQ,MAAM,IAAI,UAAU,SAAS;AAAA,MACvC;AAEA,cAAQ,MACL,MAAM,SAAS,EAAE,WAAW,mBAAmB,MAAM,CAAC,EACtD,MAAM,QAAQ,SAAS,QAAQ,CAAC;AAEnC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM;AAE9B,UAAI,OAAO;AACT,cAAM,IAAI,MAAM,4BAA4B,MAAM,OAAO,EAAE;AAAA,MAC7D;AAEA,aAAQ,QAAQ,CAAC;AAAA,IACnB;AAAA,EACF;AACF;AAMA,SAAS,WAAW,UAA0B,QAAgB;AAE5D,QAAM,cAAe,SAAgD;AACrE,QAAM,cAAe,SAAgD;AAErE,aAAO,mBAAAI,cAAqB,aAAa,aAAa;AAAA,IACpD,QAAQ;AAAA,MACN,SAAS;AAAA,QACP,iBAAiB;AAAA,MACnB;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;ACncO,SAAS,iBAAiB,UAA0B,SAA8C;AACvG,QAAM,EAAE,QAAQ,OAAO,IAAI;AAE3B,SAAO;AAAA,IACL,MAAM,YAAY,cAAwD;AACxE,UAAI,QAAQ,SACT,KAAK,UAAU,EACf,OAAO,6DAA6D,EACpE,GAAG,UAAU,WAAW,EACxB,MAAM,cAAc,QAAQ,cAAc,EAAE,YAAY,cAAc,SAAS,WAAW,MAAM,CAAC;AAEpG,UAAI,cAAc,UAAU;AAC1B,gBAAQ,MAAM,GAAG,YAAY,aAAa,QAAQ;AAAA,MACpD;AACA,UAAI,cAAc,MAAM,QAAQ;AAC9B,gBAAQ,MAAM,SAAS,QAAQ,aAAa,IAAI;AAAA,MAClD;AACA,UAAI,cAAc,OAAO;AACvB,gBAAQ,MAAM,MAAM,aAAa,KAAK;AAAA,MACxC;AACA,UAAI,cAAc,QAAQ;AACxB,gBAAQ,MAAM,MAAM,aAAa,QAAQ,aAAa,UAAU,aAAa,SAAS,MAAM,CAAC;AAAA,MAC/F;AAEA,YAAM,EAAE,KAAK,IAAI,MAAM;AACvB,aAAQ,QAAQ,CAAC;AAAA,IACnB;AAAA,IAEA,MAAM,iBAAiB,MAAuC;AAC5D,YAAM,EAAE,KAAK,IAAI,MAAM,SACpB,KAAK,UAAU,EACf,OAAO,6DAA6D,EACpE,GAAG,QAAQ,IAAI,EACf,GAAG,UAAU,WAAW,EACxB,OAAO;AACV,aAAQ,QAAoB;AAAA,IAC9B;AAAA,IAEA,MAAM,uBAA0C;AAC9C,YAAM,EAAE,KAAK,IAAI,MAAM,SACpB,KAAK,UAAU,EACf,OAAO,UAAU,EACjB,GAAG,UAAU,WAAW,EACxB,IAAI,YAAY,MAAM,IAAI;AAC7B,YAAM,aAAa,CAAC,GAAG,IAAI,KAAK,QAAQ,CAAC,GAAG,IAAI,CAAC,MAA4B,EAAE,QAAQ,CAAC,CAAC;AACzF,aAAO,WAAW,KAAK;AAAA,IACzB;AAAA,IAEA,MAAM,YAAY,QAAuD;AAGvE,YAAM,UAAU,UAAU;AAC1B,YAAM,MAAM,MAAM,MAAM,GAAG,OAAO,sBAAsB;AAAA,QACtD,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,SAAS,QAAQ,GAAG,OAAO,CAAC;AAAA,MACrD,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,MAAM,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,EAAE,OAAO,wBAAwB,EAAE;AAC7E,cAAM,IAAI,MAAM,IAAI,SAAS,uBAAuB;AAAA,MACtD;AACA,aAAO,IAAI,KAAK;AAAA,IAClB;AAAA,EACF;AACF;;;ACvCO,SAAS,gBACd,aACA,WAAkC,CAAC,GAC3B;AACR,SAAO;AACT;AAUO,SAAS,UACd,aACA,UAAoB,CAAC,GACrB,WAAW,IACH;AACR,SAAO,GAAG,WAAW;AACvB;AAMO,IAAM,gBAAgB;AAAA,EAC3B,WAAW,EAAE,OAAO,KAAK,QAAQ,KAAK,QAAQ,SAAkB,SAAS,GAAG;AAAA,EAC5E,MAAM,EAAE,OAAO,KAAK,QAAQ,KAAK,QAAQ,SAAkB,SAAS,GAAG;AAAA,EACvE,MAAM,EAAE,OAAO,MAAM,QAAQ,KAAK,QAAQ,SAAkB,SAAS,GAAG;AAAA,EACxE,IAAI,EAAE,OAAO,MAAM,QAAQ,KAAK,QAAQ,SAAkB,SAAS,GAAG;AAAA,EACtE,QAAQ,EAAE,OAAO,IAAI,QAAQ,IAAI,QAAQ,SAAkB,SAAS,GAAG;AAAA,EACvE,MAAM,EAAE,OAAO,MAAM,QAAQ,WAAoB,SAAS,GAAG;AAC/D;","names":["options","query","data","error","createSupabaseClient"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/queries.ts","../src/embed.ts","../src/shop.ts","../src/events.ts","../src/cdn.ts"],"sourcesContent":["// Server-safe exports — no React, no \"use client\"\nexport { createCmsClient } from \"./queries\"\nexport { getEmbedHtml } from \"./embed\"\nexport { createShopClient } from \"./shop\"\nexport { createEventsClient } from \"./events\"\nexport type {\n CmsEvent,\n CmsTicketTier,\n EventWithTiers,\n EventQueryOptions,\n CreateBookingParams,\n CreateBookingResult,\n EventStatus,\n BookingStatus,\n} from \"./events\"\nexport { getTransformUrl, getSrcSet, IMAGE_PRESETS } from \"./cdn\"\nexport type { ImageTransformOptions } from \"./cdn\"\nexport type {\n FieldType,\n FieldDefinition,\n Tenant,\n Profile,\n ContentType,\n ContentItem,\n MediaItem,\n MediaFolder,\n ContentQueryOptions,\n CmsClientOptions,\n TenantMembership,\n ImageConfig,\n ContentTypeSeoConfig,\n Product,\n ProductVariant,\n ProductOption,\n OrderAddress,\n CreateOrderParams,\n CreateOrderResult,\n ProductQueryOptions,\n MembershipTier,\n Member,\n GoogleReview,\n ReviewQueryOptions,\n EmbedValue,\n FlipbookPagePublic,\n FlipbookTocEntryPublic,\n FlipbookPublic,\n} from \"./types\"\n","import { createClient as createSupabaseClient } from \"@supabase/supabase-js\"\nimport type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type {\n CmsClientOptions,\n ContentItem,\n ContentQueryOptions,\n ContentType,\n GoogleReview,\n ReviewQueryOptions,\n} from \"./types\"\n\n/**\n * Creates a CMS query client authenticated with a tenant API key.\n *\n * Usage:\n * ```ts\n * const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const events = await cms.getContentItems('events', { status: 'published' })\n * ```\n *\n * The API key is sent as an `x-cms-api-key` header on every request.\n * Supabase RLS policies validate it against the tenant's stored key.\n */\nexport function createCmsClient(\n supabase: SupabaseClient,\n options: CmsClientOptions\n) {\n const { apiKey } = options\n\n // Create a new Supabase client with the API key header injected globally\n const client = withApiKey(supabase, apiKey) as SupabaseClient\n\n /** Resolve the tenant ID from the API key (cached per request) */\n let tenantIdCache: string | null = null\n\n async function getTenantId(): Promise<string> {\n if (tenantIdCache) return tenantIdCache\n\n const { data, error } = await client\n .from(\"tenants\")\n .select(\"id\")\n .single()\n\n if (error || !data) {\n throw new Error(\n \"Invalid CMS API key — no tenant found. Check your apiKey.\"\n )\n }\n\n tenantIdCache = data.id\n return data.id\n }\n\n /** Resolve a content type from its slug (cached) */\n const contentTypeCache = new Map<string, ContentType>()\n\n async function getContentTypeBySlug(contentTypeSlug: string): Promise<ContentType> {\n if (contentTypeCache.has(contentTypeSlug)) return contentTypeCache.get(contentTypeSlug)!\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n const ct = data as ContentType\n contentTypeCache.set(contentTypeSlug, ct)\n return ct\n }\n\n async function getContentTypeId(contentTypeSlug: string): Promise<string> {\n const ct = await getContentTypeBySlug(contentTypeSlug)\n return ct.id\n }\n\n /** Check if a member token has access to a gated content type */\n async function checkMemberAccess(\n contentType: ContentType,\n memberToken?: string\n ): Promise<{ allowed: boolean; memberTierId: string | null }> {\n const requiredTierId = contentType.required_membership_tier_id\n if (!requiredTierId) return { allowed: true, memberTierId: null } // Public\n\n if (!memberToken) return { allowed: false, memberTierId: null } // Gated but no token\n\n // Verify the member's token and check their tier\n const { data: session } = await client\n .from(\"member_sessions\")\n .select(\"member_id\")\n .eq(\"token_hash\", memberToken) // Note: caller should hash the token\n .single()\n\n if (!session) return { allowed: false, memberTierId: null }\n\n const { data: member } = await client\n .from(\"members\")\n .select(\"membership_tier_id, status\")\n .eq(\"id\", session.member_id)\n .single()\n\n if (!member || member.status !== \"active\") return { allowed: false, memberTierId: null }\n\n // Check if the member's tier matches (or exceeds) the required tier\n // For now: exact match or the member has the required tier\n return {\n allowed: member.membership_tier_id === requiredTierId,\n memberTierId: member.membership_tier_id as string | null,\n }\n }\n\n async function getFlipbook(id: string): Promise<import(\"./types\").FlipbookPublic | null> {\n const { data, error } = await client\n .from(\"flipbooks\")\n .select(\"id, title, page_count, status, manifest, download_enabled, tenant_id\")\n .eq(\"id\", id)\n .single()\n if (error || !data) return null\n\n const manifest = data.manifest as\n | { pages: Array<{ image: string; thumb: string; w: number; h: number }>; toc: Array<{ title: string; page: number }> }\n | null\n if (!manifest) {\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? 0,\n status: data.status as \"pending_upload\" | \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: [],\n toc: [],\n download_url: null,\n }\n }\n\n // Browser-safe env lookup via globalThis. The client package builds without\n // @types/node so referring to `process` directly fails the dts build; reading\n // through globalThis sidesteps that and works in every JS environment that\n // matters (Node, browsers, edge runtimes).\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n const cdnBase = proc?.env?.NEXT_PUBLIC_R2_PUBLIC_URL ?? \"https://cdn.distinctstudio.co.nz\"\n const prefix = `flipbooks/${data.tenant_id as string}/${data.id as string}/`\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? manifest.pages.length,\n status: data.status as \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: manifest.pages.map((p) => ({\n url: `${cdnBase}/${prefix}${p.image}`,\n thumb_url: `${cdnBase}/${prefix}${p.thumb}`,\n w: p.w,\n h: p.h,\n })),\n toc: manifest.toc,\n download_url: data.download_enabled\n ? `${options.appUrl ?? \"\"}/api/flipbooks/${data.id as string}/download`\n : null,\n }\n }\n\n return {\n /**\n * List content items for a content type.\n * Defaults to published items ordered by most recent.\n */\n async getContentItems(\n contentTypeSlug: string,\n options: ContentQueryOptions = {}\n ): Promise<(ContentItem & { locked?: boolean })[]> {\n const contentType = await getContentTypeBySlug(contentTypeSlug)\n\n const {\n status = \"published\",\n orderBy = \"published_at\",\n orderDirection = \"desc\",\n limit = 100,\n offset = 0,\n memberToken,\n } = options\n\n // Check membership access\n const access = await checkMemberAccess(contentType, memberToken)\n\n // If gated and no access, check gating mode\n if (!access.allowed && contentType.required_membership_tier_id) {\n // Get tenant settings for gating mode\n const tenantId = await getTenantId()\n const { data: settings } = await client\n .from(\"tenant_settings\")\n .select(\"membership_gating_mode\")\n .eq(\"tenant_id\", tenantId)\n .single()\n\n const mode = (settings?.membership_gating_mode as string) ?? \"teaser\"\n\n if (mode === \"hide\") {\n return [] // Hide: return nothing\n }\n\n // Teaser mode: return items with locked flag, no body/data\n let query = client\n .from(\"content_items\")\n .select(\"id, title, slug, status, excerpt, seo_title, seo_description, featured_image, published_at, created_at, updated_at\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) query = query.eq(\"status\", status)\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n if (error) throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n\n return (data ?? []).map((item) => ({\n ...item,\n data: {},\n locked: true,\n })) as (ContentItem & { locked: boolean })[]\n }\n\n // Full access\n let query = client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n }\n\n return (data ?? []) as ContentItem[]\n },\n\n /**\n * Get a single content item by its slug.\n * Returns null if not found.\n */\n async getContentItemBySlug(\n contentTypeSlug: string,\n itemSlug: string\n ): Promise<ContentItem | null> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"slug\", itemSlug)\n .single()\n\n if (error) {\n if (error.code === \"PGRST116\") return null // not found\n throw new Error(\n `Failed to fetch ${contentTypeSlug}/${itemSlug}: ${error.message}`\n )\n }\n\n return data as ContentItem\n },\n\n /**\n * Get a content type definition (including its field schema).\n */\n async getContentType(contentTypeSlug: string): Promise<ContentType> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n return data as ContentType\n },\n\n /**\n * Get all slugs for a content type (for generateStaticParams).\n */\n async getAllSlugs(\n contentTypeSlug: string\n ): Promise<{ slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"slug\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"status\", \"published\")\n\n if (error) {\n throw new Error(\n `Failed to fetch slugs for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { slug: string }[]\n },\n\n /**\n * List all content types for this tenant.\n */\n async getContentTypes(): Promise<ContentType[]> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .order(\"name\")\n\n if (error) {\n throw new Error(`Failed to fetch content types: ${error.message}`)\n }\n\n return (data ?? []) as ContentType[]\n },\n\n /**\n * Get all slug redirects for a content type.\n * Use in next.config.js redirects() or middleware for 301s.\n * Returns: [{ old_slug, new_slug }, ...]\n */\n async getRedirects(\n contentTypeSlug: string\n ): Promise<{ old_slug: string; new_slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"slug_redirects\")\n .select(\"old_slug, new_slug\")\n .eq(\"content_type_id\", contentTypeId)\n\n if (error) {\n throw new Error(\n `Failed to fetch redirects for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { old_slug: string; new_slug: string }[]\n },\n\n /**\n * Get all custom path redirects for this tenant.\n * Use alongside getRedirects() in next.config.ts for full redirect coverage.\n */\n async getCustomRedirects(): Promise<\n { source_path: string; destination_path: string; permanent: boolean }[]\n > {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"custom_redirects\")\n .select(\"source_path, destination_path, permanent\")\n .eq(\"tenant_id\", tenantId)\n\n if (error) {\n throw new Error(`Failed to fetch custom redirects: ${error.message}`)\n }\n\n return (data ?? []) as {\n source_path: string\n destination_path: string\n permanent: boolean\n }[]\n },\n\n /**\n * Get form submissions for a specific form.\n * Useful for displaying testimonials, reviews, etc.\n */\n async getFormSubmissions(\n formSlug: string,\n options: { status?: string; limit?: number; offset?: number } = {}\n ): Promise<Record<string, unknown>[]> {\n const tenantId = await getTenantId()\n\n const { data: form } = await client\n .from(\"forms\")\n .select(\"id\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n if (!form) return []\n\n const { limit = 50, offset = 0, status } = options\n\n let query = client\n .from(\"form_submissions\")\n .select(\"*\")\n .eq(\"form_id\", form.id)\n .eq(\"is_spam\", false)\n .order(\"created_at\", { ascending: false })\n .range(offset, offset + limit - 1)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n const { data } = await query\n return (data ?? []) as Record<string, unknown>[]\n },\n\n /**\n * Get a form configuration by slug.\n */\n async getForm(\n formSlug: string\n ): Promise<Record<string, unknown> | null> {\n const tenantId = await getTenantId()\n\n const { data } = await client\n .from(\"forms\")\n .select(\"id, name, slug, description, is_active\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n return data as Record<string, unknown> | null\n },\n\n /**\n * Get a flipbook by ID, including CDN-resolved page image URLs.\n * Returns null if not found or manifest not yet ready.\n */\n getFlipbook,\n\n /**\n * Get Google Reviews for this tenant.\n * Defaults to approved reviews ordered by most recent.\n */\n async getReviews(\n options: ReviewQueryOptions = {}\n ): Promise<GoogleReview[]> {\n const tenantId = await getTenantId()\n\n const {\n status = \"approved\",\n minRating,\n limit = 50,\n offset = 0,\n orderBy = \"review_timestamp\",\n orderDirection = \"desc\",\n } = options\n\n let query = client\n .from(\"google_reviews\")\n .select(\"id, author_name, author_photo_url, rating, text, review_timestamp\")\n .eq(\"tenant_id\", tenantId)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n if (minRating) {\n query = query.gte(\"rating\", minRating)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch reviews: ${error.message}`)\n }\n\n return (data ?? []) as GoogleReview[]\n },\n }\n}\n\n/**\n * Creates a new Supabase client that includes the `x-cms-api-key`\n * header in every request, using the same URL and key as the original.\n */\nfunction withApiKey(supabase: SupabaseClient, apiKey: string) {\n // Extract URL and key from the existing client\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n return createSupabaseClient(supabaseUrl, supabaseKey, {\n global: {\n headers: {\n \"x-cms-api-key\": apiKey,\n },\n },\n })\n}\n","import type { EmbedValue } from \"./types\"\n\nfunction toCssAspectRatio(ratio: string): string {\n const parts = ratio.split(\":\")\n if (parts.length !== 2) return \"16/9\"\n const w = parseFloat(parts[0])\n const h = parseFloat(parts[1])\n if (!w || !h || w <= 0 || h <= 0) return \"16/9\"\n return `${w}/${h}`\n}\n\n/**\n * Generate responsive iframe HTML for an embed field value.\n * Returns an empty string if the value is invalid or the URL is not HTTPS.\n */\nexport function getEmbedHtml(value: unknown): string {\n if (!value || typeof value !== \"object\") return \"\"\n const embed = value as EmbedValue\n if (!embed.url || !embed.url.startsWith(\"https://\")) return \"\"\n\n const width = embed.width || \"100%\"\n const aspectRatio = toCssAspectRatio(embed.aspect_ratio || \"16:9\")\n\n return `<div style=\"position:relative;width:${width};aspect-ratio:${aspectRatio}\"><iframe src=\"${embed.url}\" style=\"position:absolute;inset:0;width:100%;height:100%\" frameborder=\"0\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox\" allowfullscreen loading=\"lazy\"></iframe></div>`\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type { Product, ProductQueryOptions, CreateOrderParams, CreateOrderResult } from \"./types\"\n\nexport function createShopClient(supabase: SupabaseClient, options: { apiKey: string; appUrl?: string }) {\n const { apiKey, appUrl } = options\n\n return {\n async getProducts(queryOptions?: ProductQueryOptions): Promise<Product[]> {\n let query = supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"sort_order\", { ascending: (queryOptions?.order ?? \"asc\") === \"asc\" })\n\n if (queryOptions?.category) {\n query = query.eq(\"category\", queryOptions.category)\n }\n if (queryOptions?.tags?.length) {\n query = query.overlaps(\"tags\", queryOptions.tags)\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(queryOptions.offset, queryOptions.offset + (queryOptions.limit ?? 50) - 1)\n }\n\n const { data } = await query\n return (data ?? []) as Product[]\n },\n\n async getProductBySlug(slug: string): Promise<Product | null> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as Product) ?? null\n },\n\n async getProductCategories(): Promise<string[]> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"category\")\n .eq(\"status\", \"published\")\n .not(\"category\", \"is\", null)\n const categories = [...new Set((data ?? []).map((d: { category: string }) => d.category))]\n return categories.sort()\n },\n\n async createOrder(params: CreateOrderParams): Promise<CreateOrderResult> {\n // This must go through the API (not direct Supabase) because\n // order creation requires server-side Stripe PaymentIntent creation\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/orders/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res.json().catch(() => ({ error: \"Order creation failed\" }))\n throw new Error(err.error ?? \"Order creation failed\")\n }\n return res.json()\n },\n }\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\n\nexport type EventStatus = \"draft\" | \"published\" | \"archived\"\nexport type BookingStatus = \"pending\" | \"confirmed\" | \"cancelled\" | \"refunded\"\n\nexport interface CmsEvent {\n id: string\n tenant_id: string\n title: string\n slug: string\n description: string | null\n short_description: string | null\n status: EventStatus\n start_at: string\n end_at: string | null\n venue_name: string | null\n venue_address: string | null\n hero_image: string | null\n gallery: string[]\n tags: string[]\n seo_title: string | null\n seo_description: string | null\n metadata: Record<string, unknown>\n sort_order: number\n published_at: string | null\n created_at: string\n updated_at: string\n}\n\nexport interface CmsTicketTier {\n id: string\n event_id: string\n tenant_id: string\n name: string\n description: string | null\n price_cents: number\n capacity: number | null\n sold_count: number\n sales_start_at: string | null\n sales_end_at: string | null\n is_active: boolean\n sort_order: number\n}\n\nexport interface EventWithTiers extends CmsEvent {\n tiers: CmsTicketTier[]\n}\n\nexport interface EventQueryOptions {\n /** Only include events with start_at >= now. Default false. */\n upcomingOnly?: boolean\n tag?: string\n limit?: number\n offset?: number\n sort?: \"start_at\" | \"sort_order\" | \"created_at\"\n order?: \"asc\" | \"desc\"\n}\n\nexport interface CreateBookingParams {\n event_slug: string\n ticket_tier_id?: string\n customer_email: string\n customer_name?: string\n customer_phone?: string\n quantity?: number\n success_url?: string\n cancel_url?: string\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateBookingResult {\n booking_id: string\n booking_number: string\n status: \"pending\" | \"confirmed\"\n /** Redirect the browser here for paid bookings. */\n checkout_url?: string\n /** Present on free bookings — used for attendance QR display. */\n qr_token?: string\n}\n\nexport function createEventsClient(\n supabase: SupabaseClient,\n options: { apiKey: string; appUrl?: string }\n) {\n const { apiKey, appUrl } = options\n\n async function getEvents(\n queryOptions?: EventQueryOptions\n ): Promise<EventWithTiers[]> {\n let query = supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"start_at\", {\n ascending: (queryOptions?.order ?? \"asc\") === \"asc\",\n })\n\n if (queryOptions?.upcomingOnly) {\n query = query.gte(\"start_at\", new Date().toISOString())\n }\n if (queryOptions?.tag) {\n query = query.contains(\"tags\", [queryOptions.tag])\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(\n queryOptions.offset,\n queryOptions.offset + (queryOptions.limit ?? 50) - 1\n )\n }\n\n const { data } = await query\n return (data ?? []) as EventWithTiers[]\n }\n\n async function getEventBySlug(slug: string): Promise<EventWithTiers | null> {\n const { data } = await supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as EventWithTiers) ?? null\n }\n\n async function createBooking(\n params: CreateBookingParams\n ): Promise<CreateBookingResult> {\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/bookings/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res\n .json()\n .catch(() => ({ error: \"Booking creation failed\" }))\n throw new Error(err.error ?? \"Booking creation failed\")\n }\n return res.json() as Promise<CreateBookingResult>\n }\n\n return { getEvents, getEventBySlug, createBooking }\n}\n","/**\n * Image helpers for CMS content.\n *\n * Images are already optimised (WebP, max 2400px) on upload.\n * No server-side transforms needed — serve originals directly.\n *\n * These functions are kept for backward compatibility but no longer\n * call Supabase image transformation endpoints.\n */\n\nexport interface ImageTransformOptions {\n width?: number\n height?: number\n quality?: number\n resize?: \"contain\" | \"cover\" | \"fill\"\n format?: \"origin\"\n}\n\n/**\n * Returns the image URL directly.\n *\n * Previously this converted URLs to use Supabase's /render/image/\n * transform endpoint, but images are now pre-optimised on upload\n * (WebP, max 2400px, quality 82) so transforms are unnecessary.\n *\n * The function is kept for backward compatibility — existing code\n * that calls getTransformUrl() will continue to work without changes.\n */\nexport function getTransformUrl(\n originalUrl: string,\n _options: ImageTransformOptions = {}\n): string {\n return originalUrl\n}\n\n/**\n * Returns a simple srcSet using the original image.\n *\n * Previously generated multiple transformed widths, but since images\n * are now pre-optimised WebP, a single source is sufficient.\n * Modern browsers handle responsive display efficiently with a\n * well-sized WebP source.\n */\nexport function getSrcSet(\n originalUrl: string,\n _widths: number[] = [],\n _quality = 80\n): string {\n return `${originalUrl} 2400w`\n}\n\n/**\n * Common image size presets — kept for backward compatibility.\n * Since images are pre-optimised, these are informational only.\n */\nexport const IMAGE_PRESETS = {\n thumbnail: { width: 150, height: 150, resize: \"cover\" as const, quality: 70 },\n card: { width: 400, height: 300, resize: \"cover\" as const, quality: 80 },\n hero: { width: 1200, height: 630, resize: \"cover\" as const, quality: 85 },\n og: { width: 1200, height: 630, resize: \"cover\" as const, quality: 90 },\n avatar: { width: 80, height: 80, resize: \"cover\" as const, quality: 75 },\n full: { width: 1920, resize: \"contain\" as const, quality: 85 },\n} as const\n"],"mappings":"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","names":["options","query","data","error","createSupabaseClient"]}
package/dist/index.mjs CHANGED
@@ -44,6 +44,39 @@ function createCmsClient(supabase, options) {
44
44
  memberTierId: member.membership_tier_id
45
45
  };
46
46
  }
47
+ async function getFlipbook(id) {
48
+ const { data, error } = await client.from("flipbooks").select("id, title, page_count, status, manifest, download_enabled, tenant_id").eq("id", id).single();
49
+ if (error || !data) return null;
50
+ const manifest = data.manifest;
51
+ if (!manifest) {
52
+ return {
53
+ id: data.id,
54
+ title: data.title,
55
+ page_count: data.page_count ?? 0,
56
+ status: data.status,
57
+ page_images: [],
58
+ toc: [],
59
+ download_url: null
60
+ };
61
+ }
62
+ const proc = globalThis.process;
63
+ const cdnBase = proc?.env?.NEXT_PUBLIC_R2_PUBLIC_URL ?? "https://cdn.distinctstudio.co.nz";
64
+ const prefix = `flipbooks/${data.tenant_id}/${data.id}/`;
65
+ return {
66
+ id: data.id,
67
+ title: data.title,
68
+ page_count: data.page_count ?? manifest.pages.length,
69
+ status: data.status,
70
+ page_images: manifest.pages.map((p) => ({
71
+ url: `${cdnBase}/${prefix}${p.image}`,
72
+ thumb_url: `${cdnBase}/${prefix}${p.thumb}`,
73
+ w: p.w,
74
+ h: p.h
75
+ })),
76
+ toc: manifest.toc,
77
+ download_url: data.download_enabled ? `${options.appUrl ?? ""}/api/flipbooks/${data.id}/download` : null
78
+ };
79
+ }
47
80
  return {
48
81
  /**
49
82
  * List content items for a content type.
@@ -190,6 +223,11 @@ function createCmsClient(supabase, options) {
190
223
  const { data } = await client.from("forms").select("id, name, slug, description, is_active").eq("tenant_id", tenantId).eq("slug", formSlug).single();
191
224
  return data;
192
225
  },
226
+ /**
227
+ * Get a flipbook by ID, including CDN-resolved page image URLs.
228
+ * Returns null if not found or manifest not yet ready.
229
+ */
230
+ getFlipbook,
193
231
  /**
194
232
  * Get Google Reviews for this tenant.
195
233
  * Defaults to approved reviews ordered by most recent.
@@ -232,6 +270,24 @@ function withApiKey(supabase, apiKey) {
232
270
  });
233
271
  }
234
272
 
273
+ // src/embed.ts
274
+ function toCssAspectRatio(ratio) {
275
+ const parts = ratio.split(":");
276
+ if (parts.length !== 2) return "16/9";
277
+ const w = parseFloat(parts[0]);
278
+ const h = parseFloat(parts[1]);
279
+ if (!w || !h || w <= 0 || h <= 0) return "16/9";
280
+ return `${w}/${h}`;
281
+ }
282
+ function getEmbedHtml(value) {
283
+ if (!value || typeof value !== "object") return "";
284
+ const embed = value;
285
+ if (!embed.url || !embed.url.startsWith("https://")) return "";
286
+ const width = embed.width || "100%";
287
+ const aspectRatio = toCssAspectRatio(embed.aspect_ratio || "16:9");
288
+ return `<div style="position:relative;width:${width};aspect-ratio:${aspectRatio}"><iframe src="${embed.url}" style="position:absolute;inset:0;width:100%;height:100%" frameborder="0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" allowfullscreen loading="lazy"></iframe></div>`;
289
+ }
290
+
235
291
  // src/shop.ts
236
292
  function createShopClient(supabase, options) {
237
293
  const { apiKey, appUrl } = options;
@@ -278,6 +334,51 @@ function createShopClient(supabase, options) {
278
334
  };
279
335
  }
280
336
 
337
+ // src/events.ts
338
+ function createEventsClient(supabase, options) {
339
+ const { apiKey, appUrl } = options;
340
+ async function getEvents(queryOptions) {
341
+ let query = supabase.from("events").select("*, tiers:ticket_tiers(*)").eq("status", "published").order(queryOptions?.sort ?? "start_at", {
342
+ ascending: (queryOptions?.order ?? "asc") === "asc"
343
+ });
344
+ if (queryOptions?.upcomingOnly) {
345
+ query = query.gte("start_at", (/* @__PURE__ */ new Date()).toISOString());
346
+ }
347
+ if (queryOptions?.tag) {
348
+ query = query.contains("tags", [queryOptions.tag]);
349
+ }
350
+ if (queryOptions?.limit) {
351
+ query = query.limit(queryOptions.limit);
352
+ }
353
+ if (queryOptions?.offset) {
354
+ query = query.range(
355
+ queryOptions.offset,
356
+ queryOptions.offset + (queryOptions.limit ?? 50) - 1
357
+ );
358
+ }
359
+ const { data } = await query;
360
+ return data ?? [];
361
+ }
362
+ async function getEventBySlug(slug) {
363
+ const { data } = await supabase.from("events").select("*, tiers:ticket_tiers(*)").eq("slug", slug).eq("status", "published").single();
364
+ return data ?? null;
365
+ }
366
+ async function createBooking(params) {
367
+ const baseUrl = appUrl ?? "";
368
+ const res = await fetch(`${baseUrl}/api/bookings/create`, {
369
+ method: "POST",
370
+ headers: { "Content-Type": "application/json" },
371
+ body: JSON.stringify({ api_key: apiKey, ...params })
372
+ });
373
+ if (!res.ok) {
374
+ const err = await res.json().catch(() => ({ error: "Booking creation failed" }));
375
+ throw new Error(err.error ?? "Booking creation failed");
376
+ }
377
+ return res.json();
378
+ }
379
+ return { getEvents, getEventBySlug, createBooking };
380
+ }
381
+
281
382
  // src/cdn.ts
282
383
  function getTransformUrl(originalUrl, _options = {}) {
283
384
  return originalUrl;
@@ -296,7 +397,9 @@ var IMAGE_PRESETS = {
296
397
  export {
297
398
  IMAGE_PRESETS,
298
399
  createCmsClient,
400
+ createEventsClient,
299
401
  createShopClient,
402
+ getEmbedHtml,
300
403
  getSrcSet,
301
404
  getTransformUrl
302
405
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/queries.ts","../src/shop.ts","../src/cdn.ts"],"sourcesContent":["import { createClient as createSupabaseClient } from \"@supabase/supabase-js\"\nimport type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type {\n CmsClientOptions,\n ContentItem,\n ContentQueryOptions,\n ContentType,\n GoogleReview,\n ReviewQueryOptions,\n} from \"./types\"\n\n/**\n * Creates a CMS query client authenticated with a tenant API key.\n *\n * Usage:\n * ```ts\n * const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const events = await cms.getContentItems('events', { status: 'published' })\n * ```\n *\n * The API key is sent as an `x-cms-api-key` header on every request.\n * Supabase RLS policies validate it against the tenant's stored key.\n */\nexport function createCmsClient(\n supabase: SupabaseClient,\n options: CmsClientOptions\n) {\n const { apiKey } = options\n\n // Create a new Supabase client with the API key header injected globally\n const client = withApiKey(supabase, apiKey) as SupabaseClient\n\n /** Resolve the tenant ID from the API key (cached per request) */\n let tenantIdCache: string | null = null\n\n async function getTenantId(): Promise<string> {\n if (tenantIdCache) return tenantIdCache\n\n const { data, error } = await client\n .from(\"tenants\")\n .select(\"id\")\n .single()\n\n if (error || !data) {\n throw new Error(\n \"Invalid CMS API key — no tenant found. Check your apiKey.\"\n )\n }\n\n tenantIdCache = data.id\n return data.id\n }\n\n /** Resolve a content type from its slug (cached) */\n const contentTypeCache = new Map<string, ContentType>()\n\n async function getContentTypeBySlug(contentTypeSlug: string): Promise<ContentType> {\n if (contentTypeCache.has(contentTypeSlug)) return contentTypeCache.get(contentTypeSlug)!\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n const ct = data as ContentType\n contentTypeCache.set(contentTypeSlug, ct)\n return ct\n }\n\n async function getContentTypeId(contentTypeSlug: string): Promise<string> {\n const ct = await getContentTypeBySlug(contentTypeSlug)\n return ct.id\n }\n\n /** Check if a member token has access to a gated content type */\n async function checkMemberAccess(\n contentType: ContentType,\n memberToken?: string\n ): Promise<{ allowed: boolean; memberTierId: string | null }> {\n const requiredTierId = contentType.required_membership_tier_id\n if (!requiredTierId) return { allowed: true, memberTierId: null } // Public\n\n if (!memberToken) return { allowed: false, memberTierId: null } // Gated but no token\n\n // Verify the member's token and check their tier\n const { data: session } = await client\n .from(\"member_sessions\")\n .select(\"member_id\")\n .eq(\"token_hash\", memberToken) // Note: caller should hash the token\n .single()\n\n if (!session) return { allowed: false, memberTierId: null }\n\n const { data: member } = await client\n .from(\"members\")\n .select(\"membership_tier_id, status\")\n .eq(\"id\", session.member_id)\n .single()\n\n if (!member || member.status !== \"active\") return { allowed: false, memberTierId: null }\n\n // Check if the member's tier matches (or exceeds) the required tier\n // For now: exact match or the member has the required tier\n return {\n allowed: member.membership_tier_id === requiredTierId,\n memberTierId: member.membership_tier_id as string | null,\n }\n }\n\n return {\n /**\n * List content items for a content type.\n * Defaults to published items ordered by most recent.\n */\n async getContentItems(\n contentTypeSlug: string,\n options: ContentQueryOptions = {}\n ): Promise<(ContentItem & { locked?: boolean })[]> {\n const contentType = await getContentTypeBySlug(contentTypeSlug)\n\n const {\n status = \"published\",\n orderBy = \"published_at\",\n orderDirection = \"desc\",\n limit = 100,\n offset = 0,\n memberToken,\n } = options\n\n // Check membership access\n const access = await checkMemberAccess(contentType, memberToken)\n\n // If gated and no access, check gating mode\n if (!access.allowed && contentType.required_membership_tier_id) {\n // Get tenant settings for gating mode\n const tenantId = await getTenantId()\n const { data: settings } = await client\n .from(\"tenant_settings\")\n .select(\"membership_gating_mode\")\n .eq(\"tenant_id\", tenantId)\n .single()\n\n const mode = (settings?.membership_gating_mode as string) ?? \"teaser\"\n\n if (mode === \"hide\") {\n return [] // Hide: return nothing\n }\n\n // Teaser mode: return items with locked flag, no body/data\n let query = client\n .from(\"content_items\")\n .select(\"id, title, slug, status, excerpt, seo_title, seo_description, featured_image, published_at, created_at, updated_at\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) query = query.eq(\"status\", status)\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n if (error) throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n\n return (data ?? []).map((item) => ({\n ...item,\n data: {},\n locked: true,\n })) as (ContentItem & { locked: boolean })[]\n }\n\n // Full access\n let query = client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n }\n\n return (data ?? []) as ContentItem[]\n },\n\n /**\n * Get a single content item by its slug.\n * Returns null if not found.\n */\n async getContentItemBySlug(\n contentTypeSlug: string,\n itemSlug: string\n ): Promise<ContentItem | null> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"slug\", itemSlug)\n .single()\n\n if (error) {\n if (error.code === \"PGRST116\") return null // not found\n throw new Error(\n `Failed to fetch ${contentTypeSlug}/${itemSlug}: ${error.message}`\n )\n }\n\n return data as ContentItem\n },\n\n /**\n * Get a content type definition (including its field schema).\n */\n async getContentType(contentTypeSlug: string): Promise<ContentType> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n return data as ContentType\n },\n\n /**\n * Get all slugs for a content type (for generateStaticParams).\n */\n async getAllSlugs(\n contentTypeSlug: string\n ): Promise<{ slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"slug\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"status\", \"published\")\n\n if (error) {\n throw new Error(\n `Failed to fetch slugs for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { slug: string }[]\n },\n\n /**\n * List all content types for this tenant.\n */\n async getContentTypes(): Promise<ContentType[]> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .order(\"name\")\n\n if (error) {\n throw new Error(`Failed to fetch content types: ${error.message}`)\n }\n\n return (data ?? []) as ContentType[]\n },\n\n /**\n * Get all slug redirects for a content type.\n * Use in next.config.js redirects() or middleware for 301s.\n * Returns: [{ old_slug, new_slug }, ...]\n */\n async getRedirects(\n contentTypeSlug: string\n ): Promise<{ old_slug: string; new_slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"slug_redirects\")\n .select(\"old_slug, new_slug\")\n .eq(\"content_type_id\", contentTypeId)\n\n if (error) {\n throw new Error(\n `Failed to fetch redirects for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { old_slug: string; new_slug: string }[]\n },\n\n /**\n * Get all custom path redirects for this tenant.\n * Use alongside getRedirects() in next.config.ts for full redirect coverage.\n */\n async getCustomRedirects(): Promise<\n { source_path: string; destination_path: string; permanent: boolean }[]\n > {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"custom_redirects\")\n .select(\"source_path, destination_path, permanent\")\n .eq(\"tenant_id\", tenantId)\n\n if (error) {\n throw new Error(`Failed to fetch custom redirects: ${error.message}`)\n }\n\n return (data ?? []) as {\n source_path: string\n destination_path: string\n permanent: boolean\n }[]\n },\n\n /**\n * Get form submissions for a specific form.\n * Useful for displaying testimonials, reviews, etc.\n */\n async getFormSubmissions(\n formSlug: string,\n options: { status?: string; limit?: number; offset?: number } = {}\n ): Promise<Record<string, unknown>[]> {\n const tenantId = await getTenantId()\n\n const { data: form } = await client\n .from(\"forms\")\n .select(\"id\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n if (!form) return []\n\n const { limit = 50, offset = 0, status } = options\n\n let query = client\n .from(\"form_submissions\")\n .select(\"*\")\n .eq(\"form_id\", form.id)\n .eq(\"is_spam\", false)\n .order(\"created_at\", { ascending: false })\n .range(offset, offset + limit - 1)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n const { data } = await query\n return (data ?? []) as Record<string, unknown>[]\n },\n\n /**\n * Get a form configuration by slug.\n */\n async getForm(\n formSlug: string\n ): Promise<Record<string, unknown> | null> {\n const tenantId = await getTenantId()\n\n const { data } = await client\n .from(\"forms\")\n .select(\"id, name, slug, description, is_active\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n return data as Record<string, unknown> | null\n },\n\n /**\n * Get Google Reviews for this tenant.\n * Defaults to approved reviews ordered by most recent.\n */\n async getReviews(\n options: ReviewQueryOptions = {}\n ): Promise<GoogleReview[]> {\n const tenantId = await getTenantId()\n\n const {\n status = \"approved\",\n minRating,\n limit = 50,\n offset = 0,\n orderBy = \"review_timestamp\",\n orderDirection = \"desc\",\n } = options\n\n let query = client\n .from(\"google_reviews\")\n .select(\"id, author_name, author_photo_url, rating, text, review_timestamp\")\n .eq(\"tenant_id\", tenantId)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n if (minRating) {\n query = query.gte(\"rating\", minRating)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch reviews: ${error.message}`)\n }\n\n return (data ?? []) as GoogleReview[]\n },\n }\n}\n\n/**\n * Creates a new Supabase client that includes the `x-cms-api-key`\n * header in every request, using the same URL and key as the original.\n */\nfunction withApiKey(supabase: SupabaseClient, apiKey: string) {\n // Extract URL and key from the existing client\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n return createSupabaseClient(supabaseUrl, supabaseKey, {\n global: {\n headers: {\n \"x-cms-api-key\": apiKey,\n },\n },\n })\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type { Product, ProductQueryOptions, CreateOrderParams, CreateOrderResult } from \"./types\"\n\nexport function createShopClient(supabase: SupabaseClient, options: { apiKey: string; appUrl?: string }) {\n const { apiKey, appUrl } = options\n\n return {\n async getProducts(queryOptions?: ProductQueryOptions): Promise<Product[]> {\n let query = supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"sort_order\", { ascending: (queryOptions?.order ?? \"asc\") === \"asc\" })\n\n if (queryOptions?.category) {\n query = query.eq(\"category\", queryOptions.category)\n }\n if (queryOptions?.tags?.length) {\n query = query.overlaps(\"tags\", queryOptions.tags)\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(queryOptions.offset, queryOptions.offset + (queryOptions.limit ?? 50) - 1)\n }\n\n const { data } = await query\n return (data ?? []) as Product[]\n },\n\n async getProductBySlug(slug: string): Promise<Product | null> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as Product) ?? null\n },\n\n async getProductCategories(): Promise<string[]> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"category\")\n .eq(\"status\", \"published\")\n .not(\"category\", \"is\", null)\n const categories = [...new Set((data ?? []).map((d: { category: string }) => d.category))]\n return categories.sort()\n },\n\n async createOrder(params: CreateOrderParams): Promise<CreateOrderResult> {\n // This must go through the API (not direct Supabase) because\n // order creation requires server-side Stripe PaymentIntent creation\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/orders/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res.json().catch(() => ({ error: \"Order creation failed\" }))\n throw new Error(err.error ?? \"Order creation failed\")\n }\n return res.json()\n },\n }\n}\n","/**\n * Image helpers for CMS content.\n *\n * Images are already optimised (WebP, max 2400px) on upload.\n * No server-side transforms needed — serve originals directly.\n *\n * These functions are kept for backward compatibility but no longer\n * call Supabase image transformation endpoints.\n */\n\nexport interface ImageTransformOptions {\n width?: number\n height?: number\n quality?: number\n resize?: \"contain\" | \"cover\" | \"fill\"\n format?: \"origin\"\n}\n\n/**\n * Returns the image URL directly.\n *\n * Previously this converted URLs to use Supabase's /render/image/\n * transform endpoint, but images are now pre-optimised on upload\n * (WebP, max 2400px, quality 82) so transforms are unnecessary.\n *\n * The function is kept for backward compatibility — existing code\n * that calls getTransformUrl() will continue to work without changes.\n */\nexport function getTransformUrl(\n originalUrl: string,\n _options: ImageTransformOptions = {}\n): string {\n return originalUrl\n}\n\n/**\n * Returns a simple srcSet using the original image.\n *\n * Previously generated multiple transformed widths, but since images\n * are now pre-optimised WebP, a single source is sufficient.\n * Modern browsers handle responsive display efficiently with a\n * well-sized WebP source.\n */\nexport function getSrcSet(\n originalUrl: string,\n _widths: number[] = [],\n _quality = 80\n): string {\n return `${originalUrl} 2400w`\n}\n\n/**\n * Common image size presets — kept for backward compatibility.\n * Since images are pre-optimised, these are informational only.\n */\nexport const IMAGE_PRESETS = {\n thumbnail: { width: 150, height: 150, resize: \"cover\" as const, quality: 70 },\n card: { width: 400, height: 300, resize: \"cover\" as const, quality: 80 },\n hero: { width: 1200, height: 630, resize: \"cover\" as const, quality: 85 },\n og: { width: 1200, height: 630, resize: \"cover\" as const, quality: 90 },\n avatar: { width: 80, height: 80, resize: \"cover\" as const, quality: 75 },\n full: { width: 1920, resize: \"contain\" as const, quality: 85 },\n} as const\n"],"mappings":"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","names":["options","query","data","error"]}
1
+ {"version":3,"sources":["../src/queries.ts","../src/embed.ts","../src/shop.ts","../src/events.ts","../src/cdn.ts"],"sourcesContent":["import { createClient as createSupabaseClient } from \"@supabase/supabase-js\"\nimport type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type {\n CmsClientOptions,\n ContentItem,\n ContentQueryOptions,\n ContentType,\n GoogleReview,\n ReviewQueryOptions,\n} from \"./types\"\n\n/**\n * Creates a CMS query client authenticated with a tenant API key.\n *\n * Usage:\n * ```ts\n * const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const events = await cms.getContentItems('events', { status: 'published' })\n * ```\n *\n * The API key is sent as an `x-cms-api-key` header on every request.\n * Supabase RLS policies validate it against the tenant's stored key.\n */\nexport function createCmsClient(\n supabase: SupabaseClient,\n options: CmsClientOptions\n) {\n const { apiKey } = options\n\n // Create a new Supabase client with the API key header injected globally\n const client = withApiKey(supabase, apiKey) as SupabaseClient\n\n /** Resolve the tenant ID from the API key (cached per request) */\n let tenantIdCache: string | null = null\n\n async function getTenantId(): Promise<string> {\n if (tenantIdCache) return tenantIdCache\n\n const { data, error } = await client\n .from(\"tenants\")\n .select(\"id\")\n .single()\n\n if (error || !data) {\n throw new Error(\n \"Invalid CMS API key — no tenant found. Check your apiKey.\"\n )\n }\n\n tenantIdCache = data.id\n return data.id\n }\n\n /** Resolve a content type from its slug (cached) */\n const contentTypeCache = new Map<string, ContentType>()\n\n async function getContentTypeBySlug(contentTypeSlug: string): Promise<ContentType> {\n if (contentTypeCache.has(contentTypeSlug)) return contentTypeCache.get(contentTypeSlug)!\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n const ct = data as ContentType\n contentTypeCache.set(contentTypeSlug, ct)\n return ct\n }\n\n async function getContentTypeId(contentTypeSlug: string): Promise<string> {\n const ct = await getContentTypeBySlug(contentTypeSlug)\n return ct.id\n }\n\n /** Check if a member token has access to a gated content type */\n async function checkMemberAccess(\n contentType: ContentType,\n memberToken?: string\n ): Promise<{ allowed: boolean; memberTierId: string | null }> {\n const requiredTierId = contentType.required_membership_tier_id\n if (!requiredTierId) return { allowed: true, memberTierId: null } // Public\n\n if (!memberToken) return { allowed: false, memberTierId: null } // Gated but no token\n\n // Verify the member's token and check their tier\n const { data: session } = await client\n .from(\"member_sessions\")\n .select(\"member_id\")\n .eq(\"token_hash\", memberToken) // Note: caller should hash the token\n .single()\n\n if (!session) return { allowed: false, memberTierId: null }\n\n const { data: member } = await client\n .from(\"members\")\n .select(\"membership_tier_id, status\")\n .eq(\"id\", session.member_id)\n .single()\n\n if (!member || member.status !== \"active\") return { allowed: false, memberTierId: null }\n\n // Check if the member's tier matches (or exceeds) the required tier\n // For now: exact match or the member has the required tier\n return {\n allowed: member.membership_tier_id === requiredTierId,\n memberTierId: member.membership_tier_id as string | null,\n }\n }\n\n async function getFlipbook(id: string): Promise<import(\"./types\").FlipbookPublic | null> {\n const { data, error } = await client\n .from(\"flipbooks\")\n .select(\"id, title, page_count, status, manifest, download_enabled, tenant_id\")\n .eq(\"id\", id)\n .single()\n if (error || !data) return null\n\n const manifest = data.manifest as\n | { pages: Array<{ image: string; thumb: string; w: number; h: number }>; toc: Array<{ title: string; page: number }> }\n | null\n if (!manifest) {\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? 0,\n status: data.status as \"pending_upload\" | \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: [],\n toc: [],\n download_url: null,\n }\n }\n\n // Browser-safe env lookup via globalThis. The client package builds without\n // @types/node so referring to `process` directly fails the dts build; reading\n // through globalThis sidesteps that and works in every JS environment that\n // matters (Node, browsers, edge runtimes).\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n const cdnBase = proc?.env?.NEXT_PUBLIC_R2_PUBLIC_URL ?? \"https://cdn.distinctstudio.co.nz\"\n const prefix = `flipbooks/${data.tenant_id as string}/${data.id as string}/`\n return {\n id: data.id as string,\n title: data.title as string,\n page_count: (data.page_count as number) ?? manifest.pages.length,\n status: data.status as \"pending\" | \"processing\" | \"ready\" | \"failed\",\n page_images: manifest.pages.map((p) => ({\n url: `${cdnBase}/${prefix}${p.image}`,\n thumb_url: `${cdnBase}/${prefix}${p.thumb}`,\n w: p.w,\n h: p.h,\n })),\n toc: manifest.toc,\n download_url: data.download_enabled\n ? `${options.appUrl ?? \"\"}/api/flipbooks/${data.id as string}/download`\n : null,\n }\n }\n\n return {\n /**\n * List content items for a content type.\n * Defaults to published items ordered by most recent.\n */\n async getContentItems(\n contentTypeSlug: string,\n options: ContentQueryOptions = {}\n ): Promise<(ContentItem & { locked?: boolean })[]> {\n const contentType = await getContentTypeBySlug(contentTypeSlug)\n\n const {\n status = \"published\",\n orderBy = \"published_at\",\n orderDirection = \"desc\",\n limit = 100,\n offset = 0,\n memberToken,\n } = options\n\n // Check membership access\n const access = await checkMemberAccess(contentType, memberToken)\n\n // If gated and no access, check gating mode\n if (!access.allowed && contentType.required_membership_tier_id) {\n // Get tenant settings for gating mode\n const tenantId = await getTenantId()\n const { data: settings } = await client\n .from(\"tenant_settings\")\n .select(\"membership_gating_mode\")\n .eq(\"tenant_id\", tenantId)\n .single()\n\n const mode = (settings?.membership_gating_mode as string) ?? \"teaser\"\n\n if (mode === \"hide\") {\n return [] // Hide: return nothing\n }\n\n // Teaser mode: return items with locked flag, no body/data\n let query = client\n .from(\"content_items\")\n .select(\"id, title, slug, status, excerpt, seo_title, seo_description, featured_image, published_at, created_at, updated_at\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) query = query.eq(\"status\", status)\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n if (error) throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n\n return (data ?? []).map((item) => ({\n ...item,\n data: {},\n locked: true,\n })) as (ContentItem & { locked: boolean })[]\n }\n\n // Full access\n let query = client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentType.id)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch ${contentTypeSlug}: ${error.message}`)\n }\n\n return (data ?? []) as ContentItem[]\n },\n\n /**\n * Get a single content item by its slug.\n * Returns null if not found.\n */\n async getContentItemBySlug(\n contentTypeSlug: string,\n itemSlug: string\n ): Promise<ContentItem | null> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"*\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"slug\", itemSlug)\n .single()\n\n if (error) {\n if (error.code === \"PGRST116\") return null // not found\n throw new Error(\n `Failed to fetch ${contentTypeSlug}/${itemSlug}: ${error.message}`\n )\n }\n\n return data as ContentItem\n },\n\n /**\n * Get a content type definition (including its field schema).\n */\n async getContentType(contentTypeSlug: string): Promise<ContentType> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", contentTypeSlug)\n .single()\n\n if (error || !data) {\n throw new Error(`Content type not found: ${contentTypeSlug}`)\n }\n\n return data as ContentType\n },\n\n /**\n * Get all slugs for a content type (for generateStaticParams).\n */\n async getAllSlugs(\n contentTypeSlug: string\n ): Promise<{ slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"content_items\")\n .select(\"slug\")\n .eq(\"content_type_id\", contentTypeId)\n .eq(\"status\", \"published\")\n\n if (error) {\n throw new Error(\n `Failed to fetch slugs for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { slug: string }[]\n },\n\n /**\n * List all content types for this tenant.\n */\n async getContentTypes(): Promise<ContentType[]> {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"content_types\")\n .select(\"*\")\n .eq(\"tenant_id\", tenantId)\n .order(\"name\")\n\n if (error) {\n throw new Error(`Failed to fetch content types: ${error.message}`)\n }\n\n return (data ?? []) as ContentType[]\n },\n\n /**\n * Get all slug redirects for a content type.\n * Use in next.config.js redirects() or middleware for 301s.\n * Returns: [{ old_slug, new_slug }, ...]\n */\n async getRedirects(\n contentTypeSlug: string\n ): Promise<{ old_slug: string; new_slug: string }[]> {\n const contentTypeId = await getContentTypeId(contentTypeSlug)\n\n const { data, error } = await client\n .from(\"slug_redirects\")\n .select(\"old_slug, new_slug\")\n .eq(\"content_type_id\", contentTypeId)\n\n if (error) {\n throw new Error(\n `Failed to fetch redirects for ${contentTypeSlug}: ${error.message}`\n )\n }\n\n return (data ?? []) as { old_slug: string; new_slug: string }[]\n },\n\n /**\n * Get all custom path redirects for this tenant.\n * Use alongside getRedirects() in next.config.ts for full redirect coverage.\n */\n async getCustomRedirects(): Promise<\n { source_path: string; destination_path: string; permanent: boolean }[]\n > {\n const tenantId = await getTenantId()\n\n const { data, error } = await client\n .from(\"custom_redirects\")\n .select(\"source_path, destination_path, permanent\")\n .eq(\"tenant_id\", tenantId)\n\n if (error) {\n throw new Error(`Failed to fetch custom redirects: ${error.message}`)\n }\n\n return (data ?? []) as {\n source_path: string\n destination_path: string\n permanent: boolean\n }[]\n },\n\n /**\n * Get form submissions for a specific form.\n * Useful for displaying testimonials, reviews, etc.\n */\n async getFormSubmissions(\n formSlug: string,\n options: { status?: string; limit?: number; offset?: number } = {}\n ): Promise<Record<string, unknown>[]> {\n const tenantId = await getTenantId()\n\n const { data: form } = await client\n .from(\"forms\")\n .select(\"id\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n if (!form) return []\n\n const { limit = 50, offset = 0, status } = options\n\n let query = client\n .from(\"form_submissions\")\n .select(\"*\")\n .eq(\"form_id\", form.id)\n .eq(\"is_spam\", false)\n .order(\"created_at\", { ascending: false })\n .range(offset, offset + limit - 1)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n const { data } = await query\n return (data ?? []) as Record<string, unknown>[]\n },\n\n /**\n * Get a form configuration by slug.\n */\n async getForm(\n formSlug: string\n ): Promise<Record<string, unknown> | null> {\n const tenantId = await getTenantId()\n\n const { data } = await client\n .from(\"forms\")\n .select(\"id, name, slug, description, is_active\")\n .eq(\"tenant_id\", tenantId)\n .eq(\"slug\", formSlug)\n .single()\n\n return data as Record<string, unknown> | null\n },\n\n /**\n * Get a flipbook by ID, including CDN-resolved page image URLs.\n * Returns null if not found or manifest not yet ready.\n */\n getFlipbook,\n\n /**\n * Get Google Reviews for this tenant.\n * Defaults to approved reviews ordered by most recent.\n */\n async getReviews(\n options: ReviewQueryOptions = {}\n ): Promise<GoogleReview[]> {\n const tenantId = await getTenantId()\n\n const {\n status = \"approved\",\n minRating,\n limit = 50,\n offset = 0,\n orderBy = \"review_timestamp\",\n orderDirection = \"desc\",\n } = options\n\n let query = client\n .from(\"google_reviews\")\n .select(\"id, author_name, author_photo_url, rating, text, review_timestamp\")\n .eq(\"tenant_id\", tenantId)\n\n if (status) {\n query = query.eq(\"status\", status)\n }\n\n if (minRating) {\n query = query.gte(\"rating\", minRating)\n }\n\n query = query\n .order(orderBy, { ascending: orderDirection === \"asc\" })\n .range(offset, offset + limit - 1)\n\n const { data, error } = await query\n\n if (error) {\n throw new Error(`Failed to fetch reviews: ${error.message}`)\n }\n\n return (data ?? []) as GoogleReview[]\n },\n }\n}\n\n/**\n * Creates a new Supabase client that includes the `x-cms-api-key`\n * header in every request, using the same URL and key as the original.\n */\nfunction withApiKey(supabase: SupabaseClient, apiKey: string) {\n // Extract URL and key from the existing client\n const supabaseUrl = (supabase as unknown as { supabaseUrl: string }).supabaseUrl\n const supabaseKey = (supabase as unknown as { supabaseKey: string }).supabaseKey\n\n return createSupabaseClient(supabaseUrl, supabaseKey, {\n global: {\n headers: {\n \"x-cms-api-key\": apiKey,\n },\n },\n })\n}\n","import type { EmbedValue } from \"./types\"\n\nfunction toCssAspectRatio(ratio: string): string {\n const parts = ratio.split(\":\")\n if (parts.length !== 2) return \"16/9\"\n const w = parseFloat(parts[0])\n const h = parseFloat(parts[1])\n if (!w || !h || w <= 0 || h <= 0) return \"16/9\"\n return `${w}/${h}`\n}\n\n/**\n * Generate responsive iframe HTML for an embed field value.\n * Returns an empty string if the value is invalid or the URL is not HTTPS.\n */\nexport function getEmbedHtml(value: unknown): string {\n if (!value || typeof value !== \"object\") return \"\"\n const embed = value as EmbedValue\n if (!embed.url || !embed.url.startsWith(\"https://\")) return \"\"\n\n const width = embed.width || \"100%\"\n const aspectRatio = toCssAspectRatio(embed.aspect_ratio || \"16:9\")\n\n return `<div style=\"position:relative;width:${width};aspect-ratio:${aspectRatio}\"><iframe src=\"${embed.url}\" style=\"position:absolute;inset:0;width:100%;height:100%\" frameborder=\"0\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox\" allowfullscreen loading=\"lazy\"></iframe></div>`\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\nimport type { Product, ProductQueryOptions, CreateOrderParams, CreateOrderResult } from \"./types\"\n\nexport function createShopClient(supabase: SupabaseClient, options: { apiKey: string; appUrl?: string }) {\n const { apiKey, appUrl } = options\n\n return {\n async getProducts(queryOptions?: ProductQueryOptions): Promise<Product[]> {\n let query = supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"sort_order\", { ascending: (queryOptions?.order ?? \"asc\") === \"asc\" })\n\n if (queryOptions?.category) {\n query = query.eq(\"category\", queryOptions.category)\n }\n if (queryOptions?.tags?.length) {\n query = query.overlaps(\"tags\", queryOptions.tags)\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(queryOptions.offset, queryOptions.offset + (queryOptions.limit ?? 50) - 1)\n }\n\n const { data } = await query\n return (data ?? []) as Product[]\n },\n\n async getProductBySlug(slug: string): Promise<Product | null> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"*, variants:product_variants(*), options:product_options(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as Product) ?? null\n },\n\n async getProductCategories(): Promise<string[]> {\n const { data } = await supabase\n .from(\"products\")\n .select(\"category\")\n .eq(\"status\", \"published\")\n .not(\"category\", \"is\", null)\n const categories = [...new Set((data ?? []).map((d: { category: string }) => d.category))]\n return categories.sort()\n },\n\n async createOrder(params: CreateOrderParams): Promise<CreateOrderResult> {\n // This must go through the API (not direct Supabase) because\n // order creation requires server-side Stripe PaymentIntent creation\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/orders/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res.json().catch(() => ({ error: \"Order creation failed\" }))\n throw new Error(err.error ?? \"Order creation failed\")\n }\n return res.json()\n },\n }\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\"\n\nexport type EventStatus = \"draft\" | \"published\" | \"archived\"\nexport type BookingStatus = \"pending\" | \"confirmed\" | \"cancelled\" | \"refunded\"\n\nexport interface CmsEvent {\n id: string\n tenant_id: string\n title: string\n slug: string\n description: string | null\n short_description: string | null\n status: EventStatus\n start_at: string\n end_at: string | null\n venue_name: string | null\n venue_address: string | null\n hero_image: string | null\n gallery: string[]\n tags: string[]\n seo_title: string | null\n seo_description: string | null\n metadata: Record<string, unknown>\n sort_order: number\n published_at: string | null\n created_at: string\n updated_at: string\n}\n\nexport interface CmsTicketTier {\n id: string\n event_id: string\n tenant_id: string\n name: string\n description: string | null\n price_cents: number\n capacity: number | null\n sold_count: number\n sales_start_at: string | null\n sales_end_at: string | null\n is_active: boolean\n sort_order: number\n}\n\nexport interface EventWithTiers extends CmsEvent {\n tiers: CmsTicketTier[]\n}\n\nexport interface EventQueryOptions {\n /** Only include events with start_at >= now. Default false. */\n upcomingOnly?: boolean\n tag?: string\n limit?: number\n offset?: number\n sort?: \"start_at\" | \"sort_order\" | \"created_at\"\n order?: \"asc\" | \"desc\"\n}\n\nexport interface CreateBookingParams {\n event_slug: string\n ticket_tier_id?: string\n customer_email: string\n customer_name?: string\n customer_phone?: string\n quantity?: number\n success_url?: string\n cancel_url?: string\n metadata?: Record<string, unknown>\n}\n\nexport interface CreateBookingResult {\n booking_id: string\n booking_number: string\n status: \"pending\" | \"confirmed\"\n /** Redirect the browser here for paid bookings. */\n checkout_url?: string\n /** Present on free bookings — used for attendance QR display. */\n qr_token?: string\n}\n\nexport function createEventsClient(\n supabase: SupabaseClient,\n options: { apiKey: string; appUrl?: string }\n) {\n const { apiKey, appUrl } = options\n\n async function getEvents(\n queryOptions?: EventQueryOptions\n ): Promise<EventWithTiers[]> {\n let query = supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"status\", \"published\")\n .order(queryOptions?.sort ?? \"start_at\", {\n ascending: (queryOptions?.order ?? \"asc\") === \"asc\",\n })\n\n if (queryOptions?.upcomingOnly) {\n query = query.gte(\"start_at\", new Date().toISOString())\n }\n if (queryOptions?.tag) {\n query = query.contains(\"tags\", [queryOptions.tag])\n }\n if (queryOptions?.limit) {\n query = query.limit(queryOptions.limit)\n }\n if (queryOptions?.offset) {\n query = query.range(\n queryOptions.offset,\n queryOptions.offset + (queryOptions.limit ?? 50) - 1\n )\n }\n\n const { data } = await query\n return (data ?? []) as EventWithTiers[]\n }\n\n async function getEventBySlug(slug: string): Promise<EventWithTiers | null> {\n const { data } = await supabase\n .from(\"events\")\n .select(\"*, tiers:ticket_tiers(*)\")\n .eq(\"slug\", slug)\n .eq(\"status\", \"published\")\n .single()\n return (data as EventWithTiers) ?? null\n }\n\n async function createBooking(\n params: CreateBookingParams\n ): Promise<CreateBookingResult> {\n const baseUrl = appUrl ?? \"\"\n const res = await fetch(`${baseUrl}/api/bookings/create`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, ...params }),\n })\n if (!res.ok) {\n const err = await res\n .json()\n .catch(() => ({ error: \"Booking creation failed\" }))\n throw new Error(err.error ?? \"Booking creation failed\")\n }\n return res.json() as Promise<CreateBookingResult>\n }\n\n return { getEvents, getEventBySlug, createBooking }\n}\n","/**\n * Image helpers for CMS content.\n *\n * Images are already optimised (WebP, max 2400px) on upload.\n * No server-side transforms needed — serve originals directly.\n *\n * These functions are kept for backward compatibility but no longer\n * call Supabase image transformation endpoints.\n */\n\nexport interface ImageTransformOptions {\n width?: number\n height?: number\n quality?: number\n resize?: \"contain\" | \"cover\" | \"fill\"\n format?: \"origin\"\n}\n\n/**\n * Returns the image URL directly.\n *\n * Previously this converted URLs to use Supabase's /render/image/\n * transform endpoint, but images are now pre-optimised on upload\n * (WebP, max 2400px, quality 82) so transforms are unnecessary.\n *\n * The function is kept for backward compatibility — existing code\n * that calls getTransformUrl() will continue to work without changes.\n */\nexport function getTransformUrl(\n originalUrl: string,\n _options: ImageTransformOptions = {}\n): string {\n return originalUrl\n}\n\n/**\n * Returns a simple srcSet using the original image.\n *\n * Previously generated multiple transformed widths, but since images\n * are now pre-optimised WebP, a single source is sufficient.\n * Modern browsers handle responsive display efficiently with a\n * well-sized WebP source.\n */\nexport function getSrcSet(\n originalUrl: string,\n _widths: number[] = [],\n _quality = 80\n): string {\n return `${originalUrl} 2400w`\n}\n\n/**\n * Common image size presets — kept for backward compatibility.\n * Since images are pre-optimised, these are informational only.\n */\nexport const IMAGE_PRESETS = {\n thumbnail: { width: 150, height: 150, resize: \"cover\" as const, quality: 70 },\n card: { width: 400, height: 300, resize: \"cover\" as const, quality: 80 },\n hero: { width: 1200, height: 630, resize: \"cover\" as const, quality: 85 },\n og: { width: 1200, height: 630, resize: \"cover\" as const, quality: 90 },\n avatar: { width: 80, height: 80, resize: \"cover\" as const, quality: 75 },\n full: { width: 1920, resize: \"contain\" as const, quality: 85 },\n} as const\n"],"mappings":"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","names":["options","query","data","error"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distinctagency/cms-client",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "description": "Client library for Distinct CMS — query content, products, and manage orders",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -20,10 +20,6 @@
20
20
  "require": "./dist/client.js"
21
21
  }
22
22
  },
23
- "scripts": {
24
- "build": "tsup",
25
- "prepublishOnly": "pnpm build"
26
- },
27
23
  "peerDependencies": {
28
24
  "@supabase/supabase-js": ">=2.0.0",
29
25
  "next": ">=14.0.0",
@@ -49,5 +45,8 @@
49
45
  ],
50
46
  "publishConfig": {
51
47
  "access": "public"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup"
52
51
  }
53
- }
52
+ }