@distinctagency/cms-client 1.9.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -0
- package/dist/index.mjs.map +1 -1
- package/dist/tracking-scripts.d.mts +58 -0
- package/dist/tracking-scripts.d.ts +58 -0
- package/dist/tracking-scripts.js +84 -0
- package/dist/tracking-scripts.js.map +1 -0
- package/dist/tracking-scripts.mjs +58 -0
- package/dist/tracking-scripts.mjs.map +1 -0
- package/package.json +6 -1
package/dist/index.d.mts
CHANGED
|
@@ -387,7 +387,17 @@ declare function createCmsClient(supabase: SupabaseClient, options: CmsClientOpt
|
|
|
387
387
|
* Defaults to approved reviews ordered by most recent.
|
|
388
388
|
*/
|
|
389
389
|
getReviews(options?: ReviewQueryOptions): Promise<GoogleReview[]>;
|
|
390
|
+
/**
|
|
391
|
+
* Get the tenant's third-party tracking IDs (Google Analytics, Google Tag Manager, Meta Pixel).
|
|
392
|
+
* Each value is null when not configured. These IDs are public and safe to render in HTML.
|
|
393
|
+
*/
|
|
394
|
+
getTrackingConfig(): Promise<TrackingConfig>;
|
|
390
395
|
};
|
|
396
|
+
interface TrackingConfig {
|
|
397
|
+
googleAnalyticsId: string | null;
|
|
398
|
+
googleTagManagerId: string | null;
|
|
399
|
+
metaPixelId: string | null;
|
|
400
|
+
}
|
|
391
401
|
|
|
392
402
|
/**
|
|
393
403
|
* Generate responsive iframe HTML for an embed field value.
|
|
@@ -563,4 +573,4 @@ declare const IMAGE_PRESETS: {
|
|
|
563
573
|
};
|
|
564
574
|
};
|
|
565
575
|
|
|
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 };
|
|
576
|
+
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, type TrackingConfig, createCmsClient, createEventsClient, createShopClient, getEmbedHtml, getSrcSet, getTransformUrl };
|
package/dist/index.d.ts
CHANGED
|
@@ -387,7 +387,17 @@ declare function createCmsClient(supabase: SupabaseClient, options: CmsClientOpt
|
|
|
387
387
|
* Defaults to approved reviews ordered by most recent.
|
|
388
388
|
*/
|
|
389
389
|
getReviews(options?: ReviewQueryOptions): Promise<GoogleReview[]>;
|
|
390
|
+
/**
|
|
391
|
+
* Get the tenant's third-party tracking IDs (Google Analytics, Google Tag Manager, Meta Pixel).
|
|
392
|
+
* Each value is null when not configured. These IDs are public and safe to render in HTML.
|
|
393
|
+
*/
|
|
394
|
+
getTrackingConfig(): Promise<TrackingConfig>;
|
|
390
395
|
};
|
|
396
|
+
interface TrackingConfig {
|
|
397
|
+
googleAnalyticsId: string | null;
|
|
398
|
+
googleTagManagerId: string | null;
|
|
399
|
+
metaPixelId: string | null;
|
|
400
|
+
}
|
|
391
401
|
|
|
392
402
|
/**
|
|
393
403
|
* Generate responsive iframe HTML for an embed field value.
|
|
@@ -563,4 +573,4 @@ declare const IMAGE_PRESETS: {
|
|
|
563
573
|
};
|
|
564
574
|
};
|
|
565
575
|
|
|
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 };
|
|
576
|
+
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, type TrackingConfig, createCmsClient, createEventsClient, createShopClient, getEmbedHtml, getSrcSet, getTransformUrl };
|
package/dist/index.js
CHANGED
|
@@ -287,9 +287,27 @@ function createCmsClient(supabase, options) {
|
|
|
287
287
|
throw new Error(`Failed to fetch reviews: ${error.message}`);
|
|
288
288
|
}
|
|
289
289
|
return data ?? [];
|
|
290
|
+
},
|
|
291
|
+
/**
|
|
292
|
+
* Get the tenant's third-party tracking IDs (Google Analytics, Google Tag Manager, Meta Pixel).
|
|
293
|
+
* Each value is null when not configured. These IDs are public and safe to render in HTML.
|
|
294
|
+
*/
|
|
295
|
+
async getTrackingConfig() {
|
|
296
|
+
const tenantId = await getTenantId();
|
|
297
|
+
const { data } = await client.from("tenant_settings").select("google_analytics_id, google_tag_manager_id, meta_pixel_id").eq("tenant_id", tenantId).maybeSingle();
|
|
298
|
+
return {
|
|
299
|
+
googleAnalyticsId: nullify(data?.google_analytics_id),
|
|
300
|
+
googleTagManagerId: nullify(data?.google_tag_manager_id),
|
|
301
|
+
metaPixelId: nullify(data?.meta_pixel_id)
|
|
302
|
+
};
|
|
290
303
|
}
|
|
291
304
|
};
|
|
292
305
|
}
|
|
306
|
+
function nullify(v) {
|
|
307
|
+
if (!v) return null;
|
|
308
|
+
const trimmed = v.trim();
|
|
309
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
310
|
+
}
|
|
293
311
|
function withApiKey(supabase, apiKey) {
|
|
294
312
|
const supabaseUrl = supabase.supabaseUrl;
|
|
295
313
|
const supabaseKey = supabase.supabaseKey;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
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 type { TrackingConfig } 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 * Get the tenant's third-party tracking IDs (Google Analytics, Google Tag Manager, Meta Pixel).\n * Each value is null when not configured. These IDs are public and safe to render in HTML.\n */\n async getTrackingConfig(): Promise<TrackingConfig> {\n const tenantId = await getTenantId()\n const { data } = await client\n .from(\"tenant_settings\")\n .select(\"google_analytics_id, google_tag_manager_id, meta_pixel_id\")\n .eq(\"tenant_id\", tenantId)\n .maybeSingle()\n\n return {\n googleAnalyticsId: nullify(data?.google_analytics_id as string | null | undefined),\n googleTagManagerId: nullify(data?.google_tag_manager_id as string | null | undefined),\n metaPixelId: nullify(data?.meta_pixel_id as string | null | undefined),\n }\n },\n }\n}\n\nfunction nullify(v: string | null | undefined): string | null {\n if (!v) return null\n const trimmed = v.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport interface TrackingConfig {\n googleAnalyticsId: string | null\n googleTagManagerId: string | null\n metaPixelId: string | null\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
|
@@ -255,9 +255,27 @@ function createCmsClient(supabase, options) {
|
|
|
255
255
|
throw new Error(`Failed to fetch reviews: ${error.message}`);
|
|
256
256
|
}
|
|
257
257
|
return data ?? [];
|
|
258
|
+
},
|
|
259
|
+
/**
|
|
260
|
+
* Get the tenant's third-party tracking IDs (Google Analytics, Google Tag Manager, Meta Pixel).
|
|
261
|
+
* Each value is null when not configured. These IDs are public and safe to render in HTML.
|
|
262
|
+
*/
|
|
263
|
+
async getTrackingConfig() {
|
|
264
|
+
const tenantId = await getTenantId();
|
|
265
|
+
const { data } = await client.from("tenant_settings").select("google_analytics_id, google_tag_manager_id, meta_pixel_id").eq("tenant_id", tenantId).maybeSingle();
|
|
266
|
+
return {
|
|
267
|
+
googleAnalyticsId: nullify(data?.google_analytics_id),
|
|
268
|
+
googleTagManagerId: nullify(data?.google_tag_manager_id),
|
|
269
|
+
metaPixelId: nullify(data?.meta_pixel_id)
|
|
270
|
+
};
|
|
258
271
|
}
|
|
259
272
|
};
|
|
260
273
|
}
|
|
274
|
+
function nullify(v) {
|
|
275
|
+
if (!v) return null;
|
|
276
|
+
const trimmed = v.trim();
|
|
277
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
278
|
+
}
|
|
261
279
|
function withApiKey(supabase, apiKey) {
|
|
262
280
|
const supabaseUrl = supabase.supabaseUrl;
|
|
263
281
|
const supabaseKey = supabase.supabaseKey;
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
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 * Get the tenant's third-party tracking IDs (Google Analytics, Google Tag Manager, Meta Pixel).\n * Each value is null when not configured. These IDs are public and safe to render in HTML.\n */\n async getTrackingConfig(): Promise<TrackingConfig> {\n const tenantId = await getTenantId()\n const { data } = await client\n .from(\"tenant_settings\")\n .select(\"google_analytics_id, google_tag_manager_id, meta_pixel_id\")\n .eq(\"tenant_id\", tenantId)\n .maybeSingle()\n\n return {\n googleAnalyticsId: nullify(data?.google_analytics_id as string | null | undefined),\n googleTagManagerId: nullify(data?.google_tag_manager_id as string | null | undefined),\n metaPixelId: nullify(data?.meta_pixel_id as string | null | undefined),\n }\n },\n }\n}\n\nfunction nullify(v: string | null | undefined): string | null {\n if (!v) return null\n const trimmed = v.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport interface TrackingConfig {\n googleAnalyticsId: string | null\n googleTagManagerId: string | null\n metaPixelId: string | null\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"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface TrackingConfig {
|
|
4
|
+
googleAnalyticsId: string | null;
|
|
5
|
+
googleTagManagerId: string | null;
|
|
6
|
+
metaPixelId: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface TrackingScriptsProps {
|
|
10
|
+
config: TrackingConfig;
|
|
11
|
+
/**
|
|
12
|
+
* When both GTM and GA4 are set, by default only GTM is rendered (configure
|
|
13
|
+
* GA4 inside GTM to avoid double-counting). Set this to `true` to force-render
|
|
14
|
+
* the GA4 snippet alongside GTM.
|
|
15
|
+
*/
|
|
16
|
+
forceGa4WithGtm?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Drop-in script injection for Google Tag Manager, Google Analytics 4, and
|
|
20
|
+
* Meta Pixel. Each block renders only when its corresponding ID is set.
|
|
21
|
+
*
|
|
22
|
+
* Usage in `app/layout.tsx`:
|
|
23
|
+
* ```tsx
|
|
24
|
+
* import { createCmsClient } from '@distinctagency/cms-client'
|
|
25
|
+
* import { TrackingScripts } from '@distinctagency/cms-client/tracking-scripts'
|
|
26
|
+
*
|
|
27
|
+
* export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
28
|
+
* const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })
|
|
29
|
+
* const tracking = await cms.getTrackingConfig()
|
|
30
|
+
* return (
|
|
31
|
+
* <html>
|
|
32
|
+
* <head>
|
|
33
|
+
* <TrackingScripts config={tracking} />
|
|
34
|
+
* </head>
|
|
35
|
+
* <body>{children}</body>
|
|
36
|
+
* </html>
|
|
37
|
+
* )
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare function TrackingScripts({ config, forceGa4WithGtm }: TrackingScriptsProps): react_jsx_runtime.JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* Renders the GTM `<noscript>` iframe that should live immediately after the
|
|
44
|
+
* opening `<body>` tag. Use this alongside `<TrackingScripts />` if you also
|
|
45
|
+
* want to support no-JS visitors.
|
|
46
|
+
*
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <body>
|
|
49
|
+
* <TrackingNoScript config={tracking} />
|
|
50
|
+
* ...
|
|
51
|
+
* </body>
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
declare function TrackingNoScript({ config }: {
|
|
55
|
+
config: TrackingConfig;
|
|
56
|
+
}): react_jsx_runtime.JSX.Element;
|
|
57
|
+
|
|
58
|
+
export { TrackingNoScript, TrackingScripts };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface TrackingConfig {
|
|
4
|
+
googleAnalyticsId: string | null;
|
|
5
|
+
googleTagManagerId: string | null;
|
|
6
|
+
metaPixelId: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface TrackingScriptsProps {
|
|
10
|
+
config: TrackingConfig;
|
|
11
|
+
/**
|
|
12
|
+
* When both GTM and GA4 are set, by default only GTM is rendered (configure
|
|
13
|
+
* GA4 inside GTM to avoid double-counting). Set this to `true` to force-render
|
|
14
|
+
* the GA4 snippet alongside GTM.
|
|
15
|
+
*/
|
|
16
|
+
forceGa4WithGtm?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Drop-in script injection for Google Tag Manager, Google Analytics 4, and
|
|
20
|
+
* Meta Pixel. Each block renders only when its corresponding ID is set.
|
|
21
|
+
*
|
|
22
|
+
* Usage in `app/layout.tsx`:
|
|
23
|
+
* ```tsx
|
|
24
|
+
* import { createCmsClient } from '@distinctagency/cms-client'
|
|
25
|
+
* import { TrackingScripts } from '@distinctagency/cms-client/tracking-scripts'
|
|
26
|
+
*
|
|
27
|
+
* export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
28
|
+
* const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })
|
|
29
|
+
* const tracking = await cms.getTrackingConfig()
|
|
30
|
+
* return (
|
|
31
|
+
* <html>
|
|
32
|
+
* <head>
|
|
33
|
+
* <TrackingScripts config={tracking} />
|
|
34
|
+
* </head>
|
|
35
|
+
* <body>{children}</body>
|
|
36
|
+
* </html>
|
|
37
|
+
* )
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare function TrackingScripts({ config, forceGa4WithGtm }: TrackingScriptsProps): react_jsx_runtime.JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* Renders the GTM `<noscript>` iframe that should live immediately after the
|
|
44
|
+
* opening `<body>` tag. Use this alongside `<TrackingScripts />` if you also
|
|
45
|
+
* want to support no-JS visitors.
|
|
46
|
+
*
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <body>
|
|
49
|
+
* <TrackingNoScript config={tracking} />
|
|
50
|
+
* ...
|
|
51
|
+
* </body>
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
declare function TrackingNoScript({ config }: {
|
|
55
|
+
config: TrackingConfig;
|
|
56
|
+
}): react_jsx_runtime.JSX.Element;
|
|
57
|
+
|
|
58
|
+
export { TrackingNoScript, TrackingScripts };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/tracking-scripts.tsx
|
|
21
|
+
var tracking_scripts_exports = {};
|
|
22
|
+
__export(tracking_scripts_exports, {
|
|
23
|
+
TrackingNoScript: () => TrackingNoScript,
|
|
24
|
+
TrackingScripts: () => TrackingScripts
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(tracking_scripts_exports);
|
|
27
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
28
|
+
function TrackingScripts({ config, forceGa4WithGtm = false }) {
|
|
29
|
+
const { googleAnalyticsId, googleTagManagerId, metaPixelId } = config;
|
|
30
|
+
const renderGa4 = !!googleAnalyticsId && (forceGa4WithGtm || !googleTagManagerId);
|
|
31
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
32
|
+
googleTagManagerId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GtmHeadScript, { id: googleTagManagerId }),
|
|
33
|
+
renderGa4 && googleAnalyticsId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Ga4Script, { id: googleAnalyticsId }),
|
|
34
|
+
metaPixelId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetaPixelScript, { id: metaPixelId })
|
|
35
|
+
] });
|
|
36
|
+
}
|
|
37
|
+
function TrackingNoScript({ config }) {
|
|
38
|
+
const { googleTagManagerId, metaPixelId } = config;
|
|
39
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
40
|
+
googleTagManagerId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("noscript", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
41
|
+
"iframe",
|
|
42
|
+
{
|
|
43
|
+
src: `https://www.googletagmanager.com/ns.html?id=${encodeURIComponent(googleTagManagerId)}`,
|
|
44
|
+
height: "0",
|
|
45
|
+
width: "0",
|
|
46
|
+
style: { display: "none", visibility: "hidden" }
|
|
47
|
+
}
|
|
48
|
+
) }),
|
|
49
|
+
metaPixelId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("noscript", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
50
|
+
"img",
|
|
51
|
+
{
|
|
52
|
+
height: "1",
|
|
53
|
+
width: "1",
|
|
54
|
+
style: { display: "none" },
|
|
55
|
+
alt: "",
|
|
56
|
+
src: `https://www.facebook.com/tr?id=${encodeURIComponent(metaPixelId)}&ev=PageView&noscript=1`
|
|
57
|
+
}
|
|
58
|
+
) })
|
|
59
|
+
] });
|
|
60
|
+
}
|
|
61
|
+
function GtmHeadScript({ id }) {
|
|
62
|
+
const escaped = JSON.stringify(id);
|
|
63
|
+
const html = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer',${escaped});`;
|
|
64
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("script", { dangerouslySetInnerHTML: { __html: html } });
|
|
65
|
+
}
|
|
66
|
+
function Ga4Script({ id }) {
|
|
67
|
+
const safeId = JSON.stringify(id);
|
|
68
|
+
const html = `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', ${safeId});`;
|
|
69
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
70
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("script", { async: true, src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}` }),
|
|
71
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("script", { dangerouslySetInnerHTML: { __html: html } })
|
|
72
|
+
] });
|
|
73
|
+
}
|
|
74
|
+
function MetaPixelScript({ id }) {
|
|
75
|
+
const safeId = JSON.stringify(id);
|
|
76
|
+
const html = `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init', ${safeId});fbq('track', 'PageView');`;
|
|
77
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("script", { dangerouslySetInnerHTML: { __html: html } });
|
|
78
|
+
}
|
|
79
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
80
|
+
0 && (module.exports = {
|
|
81
|
+
TrackingNoScript,
|
|
82
|
+
TrackingScripts
|
|
83
|
+
});
|
|
84
|
+
//# sourceMappingURL=tracking-scripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tracking-scripts.tsx"],"sourcesContent":["// Server-safe React component — renders inline <script> tags for third-party\n// trackers based on tenant config from cms.getTrackingConfig(). No \"use client\"\n// directive: this is meant to render in the document <head> from a Server\n// Component (e.g. app/layout.tsx).\n\nimport type { TrackingConfig } from \"./queries\"\n\ninterface TrackingScriptsProps {\n config: TrackingConfig\n /**\n * When both GTM and GA4 are set, by default only GTM is rendered (configure\n * GA4 inside GTM to avoid double-counting). Set this to `true` to force-render\n * the GA4 snippet alongside GTM.\n */\n forceGa4WithGtm?: boolean\n}\n\n/**\n * Drop-in script injection for Google Tag Manager, Google Analytics 4, and\n * Meta Pixel. Each block renders only when its corresponding ID is set.\n *\n * Usage in `app/layout.tsx`:\n * ```tsx\n * import { createCmsClient } from '@distinctagency/cms-client'\n * import { TrackingScripts } from '@distinctagency/cms-client/tracking-scripts'\n *\n * export default async function RootLayout({ children }: { children: React.ReactNode }) {\n * const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const tracking = await cms.getTrackingConfig()\n * return (\n * <html>\n * <head>\n * <TrackingScripts config={tracking} />\n * </head>\n * <body>{children}</body>\n * </html>\n * )\n * }\n * ```\n */\nexport function TrackingScripts({ config, forceGa4WithGtm = false }: TrackingScriptsProps) {\n const { googleAnalyticsId, googleTagManagerId, metaPixelId } = config\n const renderGa4 = !!googleAnalyticsId && (forceGa4WithGtm || !googleTagManagerId)\n\n return (\n <>\n {googleTagManagerId && <GtmHeadScript id={googleTagManagerId} />}\n {renderGa4 && googleAnalyticsId && <Ga4Script id={googleAnalyticsId} />}\n {metaPixelId && <MetaPixelScript id={metaPixelId} />}\n </>\n )\n}\n\n/**\n * Renders the GTM `<noscript>` iframe that should live immediately after the\n * opening `<body>` tag. Use this alongside `<TrackingScripts />` if you also\n * want to support no-JS visitors.\n *\n * ```tsx\n * <body>\n * <TrackingNoScript config={tracking} />\n * ...\n * </body>\n * ```\n */\nexport function TrackingNoScript({ config }: { config: TrackingConfig }) {\n const { googleTagManagerId, metaPixelId } = config\n return (\n <>\n {googleTagManagerId && (\n <noscript>\n <iframe\n src={`https://www.googletagmanager.com/ns.html?id=${encodeURIComponent(googleTagManagerId)}`}\n height=\"0\"\n width=\"0\"\n style={{ display: \"none\", visibility: \"hidden\" }}\n />\n </noscript>\n )}\n {metaPixelId && (\n <noscript>\n <img\n height=\"1\"\n width=\"1\"\n style={{ display: \"none\" }}\n alt=\"\"\n src={`https://www.facebook.com/tr?id=${encodeURIComponent(metaPixelId)}&ev=PageView&noscript=1`}\n />\n </noscript>\n )}\n </>\n )\n}\n\nfunction GtmHeadScript({ id }: { id: string }) {\n const escaped = JSON.stringify(id)\n const html = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer',${escaped});`\n return <script dangerouslySetInnerHTML={{ __html: html }} />\n}\n\nfunction Ga4Script({ id }: { id: string }) {\n const safeId = JSON.stringify(id)\n const html = `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', ${safeId});`\n return (\n <>\n <script async src={`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`} />\n <script dangerouslySetInnerHTML={{ __html: html }} />\n </>\n )\n}\n\nfunction MetaPixelScript({ id }: { id: string }) {\n const safeId = JSON.stringify(id)\n const html = `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init', ${safeId});fbq('track', 'PageView');`\n return <script dangerouslySetInnerHTML={{ __html: html }} />\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6CI;AALG,SAAS,gBAAgB,EAAE,QAAQ,kBAAkB,MAAM,GAAyB;AACzF,QAAM,EAAE,mBAAmB,oBAAoB,YAAY,IAAI;AAC/D,QAAM,YAAY,CAAC,CAAC,sBAAsB,mBAAmB,CAAC;AAE9D,SACE,4EACG;AAAA,0BAAsB,4CAAC,iBAAc,IAAI,oBAAoB;AAAA,IAC7D,aAAa,qBAAqB,4CAAC,aAAU,IAAI,mBAAmB;AAAA,IACpE,eAAe,4CAAC,mBAAgB,IAAI,aAAa;AAAA,KACpD;AAEJ;AAcO,SAAS,iBAAiB,EAAE,OAAO,GAA+B;AACvE,QAAM,EAAE,oBAAoB,YAAY,IAAI;AAC5C,SACE,4EACG;AAAA,0BACC,4CAAC,cACC;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,+CAA+C,mBAAmB,kBAAkB,CAAC;AAAA,QAC1F,QAAO;AAAA,QACP,OAAM;AAAA,QACN,OAAO,EAAE,SAAS,QAAQ,YAAY,SAAS;AAAA;AAAA,IACjD,GACF;AAAA,IAED,eACC,4CAAC,cACC;AAAA,MAAC;AAAA;AAAA,QACC,QAAO;AAAA,QACP,OAAM;AAAA,QACN,OAAO,EAAE,SAAS,OAAO;AAAA,QACzB,KAAI;AAAA,QACJ,KAAK,kCAAkC,mBAAmB,WAAW,CAAC;AAAA;AAAA,IACxE,GACF;AAAA,KAEJ;AAEJ;AAEA,SAAS,cAAc,EAAE,GAAG,GAAmB;AAC7C,QAAM,UAAU,KAAK,UAAU,EAAE;AACjC,QAAM,OAAO,sUAAsU,OAAO;AAC1V,SAAO,4CAAC,YAAO,yBAAyB,EAAE,QAAQ,KAAK,GAAG;AAC5D;AAEA,SAAS,UAAU,EAAE,GAAG,GAAmB;AACzC,QAAM,SAAS,KAAK,UAAU,EAAE;AAChC,QAAM,OAAO,8HAA8H,MAAM;AACjJ,SACE,4EACE;AAAA,gDAAC,YAAO,OAAK,MAAC,KAAK,+CAA+C,mBAAmB,EAAE,CAAC,IAAI;AAAA,IAC5F,4CAAC,YAAO,yBAAyB,EAAE,QAAQ,KAAK,GAAG;AAAA,KACrD;AAEJ;AAEA,SAAS,gBAAgB,EAAE,GAAG,GAAmB;AAC/C,QAAM,SAAS,KAAK,UAAU,EAAE;AAChC,QAAM,OAAO,uYAAuY,MAAM;AAC1Z,SAAO,4CAAC,YAAO,yBAAyB,EAAE,QAAQ,KAAK,GAAG;AAC5D;","names":[]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/tracking-scripts.tsx
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
function TrackingScripts({ config, forceGa4WithGtm = false }) {
|
|
4
|
+
const { googleAnalyticsId, googleTagManagerId, metaPixelId } = config;
|
|
5
|
+
const renderGa4 = !!googleAnalyticsId && (forceGa4WithGtm || !googleTagManagerId);
|
|
6
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7
|
+
googleTagManagerId && /* @__PURE__ */ jsx(GtmHeadScript, { id: googleTagManagerId }),
|
|
8
|
+
renderGa4 && googleAnalyticsId && /* @__PURE__ */ jsx(Ga4Script, { id: googleAnalyticsId }),
|
|
9
|
+
metaPixelId && /* @__PURE__ */ jsx(MetaPixelScript, { id: metaPixelId })
|
|
10
|
+
] });
|
|
11
|
+
}
|
|
12
|
+
function TrackingNoScript({ config }) {
|
|
13
|
+
const { googleTagManagerId, metaPixelId } = config;
|
|
14
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
15
|
+
googleTagManagerId && /* @__PURE__ */ jsx("noscript", { children: /* @__PURE__ */ jsx(
|
|
16
|
+
"iframe",
|
|
17
|
+
{
|
|
18
|
+
src: `https://www.googletagmanager.com/ns.html?id=${encodeURIComponent(googleTagManagerId)}`,
|
|
19
|
+
height: "0",
|
|
20
|
+
width: "0",
|
|
21
|
+
style: { display: "none", visibility: "hidden" }
|
|
22
|
+
}
|
|
23
|
+
) }),
|
|
24
|
+
metaPixelId && /* @__PURE__ */ jsx("noscript", { children: /* @__PURE__ */ jsx(
|
|
25
|
+
"img",
|
|
26
|
+
{
|
|
27
|
+
height: "1",
|
|
28
|
+
width: "1",
|
|
29
|
+
style: { display: "none" },
|
|
30
|
+
alt: "",
|
|
31
|
+
src: `https://www.facebook.com/tr?id=${encodeURIComponent(metaPixelId)}&ev=PageView&noscript=1`
|
|
32
|
+
}
|
|
33
|
+
) })
|
|
34
|
+
] });
|
|
35
|
+
}
|
|
36
|
+
function GtmHeadScript({ id }) {
|
|
37
|
+
const escaped = JSON.stringify(id);
|
|
38
|
+
const html = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer',${escaped});`;
|
|
39
|
+
return /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: html } });
|
|
40
|
+
}
|
|
41
|
+
function Ga4Script({ id }) {
|
|
42
|
+
const safeId = JSON.stringify(id);
|
|
43
|
+
const html = `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', ${safeId});`;
|
|
44
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
45
|
+
/* @__PURE__ */ jsx("script", { async: true, src: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}` }),
|
|
46
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: html } })
|
|
47
|
+
] });
|
|
48
|
+
}
|
|
49
|
+
function MetaPixelScript({ id }) {
|
|
50
|
+
const safeId = JSON.stringify(id);
|
|
51
|
+
const html = `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init', ${safeId});fbq('track', 'PageView');`;
|
|
52
|
+
return /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: html } });
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
TrackingNoScript,
|
|
56
|
+
TrackingScripts
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=tracking-scripts.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tracking-scripts.tsx"],"sourcesContent":["// Server-safe React component — renders inline <script> tags for third-party\n// trackers based on tenant config from cms.getTrackingConfig(). No \"use client\"\n// directive: this is meant to render in the document <head> from a Server\n// Component (e.g. app/layout.tsx).\n\nimport type { TrackingConfig } from \"./queries\"\n\ninterface TrackingScriptsProps {\n config: TrackingConfig\n /**\n * When both GTM and GA4 are set, by default only GTM is rendered (configure\n * GA4 inside GTM to avoid double-counting). Set this to `true` to force-render\n * the GA4 snippet alongside GTM.\n */\n forceGa4WithGtm?: boolean\n}\n\n/**\n * Drop-in script injection for Google Tag Manager, Google Analytics 4, and\n * Meta Pixel. Each block renders only when its corresponding ID is set.\n *\n * Usage in `app/layout.tsx`:\n * ```tsx\n * import { createCmsClient } from '@distinctagency/cms-client'\n * import { TrackingScripts } from '@distinctagency/cms-client/tracking-scripts'\n *\n * export default async function RootLayout({ children }: { children: React.ReactNode }) {\n * const cms = createCmsClient(supabase, { apiKey: process.env.CMS_API_KEY! })\n * const tracking = await cms.getTrackingConfig()\n * return (\n * <html>\n * <head>\n * <TrackingScripts config={tracking} />\n * </head>\n * <body>{children}</body>\n * </html>\n * )\n * }\n * ```\n */\nexport function TrackingScripts({ config, forceGa4WithGtm = false }: TrackingScriptsProps) {\n const { googleAnalyticsId, googleTagManagerId, metaPixelId } = config\n const renderGa4 = !!googleAnalyticsId && (forceGa4WithGtm || !googleTagManagerId)\n\n return (\n <>\n {googleTagManagerId && <GtmHeadScript id={googleTagManagerId} />}\n {renderGa4 && googleAnalyticsId && <Ga4Script id={googleAnalyticsId} />}\n {metaPixelId && <MetaPixelScript id={metaPixelId} />}\n </>\n )\n}\n\n/**\n * Renders the GTM `<noscript>` iframe that should live immediately after the\n * opening `<body>` tag. Use this alongside `<TrackingScripts />` if you also\n * want to support no-JS visitors.\n *\n * ```tsx\n * <body>\n * <TrackingNoScript config={tracking} />\n * ...\n * </body>\n * ```\n */\nexport function TrackingNoScript({ config }: { config: TrackingConfig }) {\n const { googleTagManagerId, metaPixelId } = config\n return (\n <>\n {googleTagManagerId && (\n <noscript>\n <iframe\n src={`https://www.googletagmanager.com/ns.html?id=${encodeURIComponent(googleTagManagerId)}`}\n height=\"0\"\n width=\"0\"\n style={{ display: \"none\", visibility: \"hidden\" }}\n />\n </noscript>\n )}\n {metaPixelId && (\n <noscript>\n <img\n height=\"1\"\n width=\"1\"\n style={{ display: \"none\" }}\n alt=\"\"\n src={`https://www.facebook.com/tr?id=${encodeURIComponent(metaPixelId)}&ev=PageView&noscript=1`}\n />\n </noscript>\n )}\n </>\n )\n}\n\nfunction GtmHeadScript({ id }: { id: string }) {\n const escaped = JSON.stringify(id)\n const html = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer',${escaped});`\n return <script dangerouslySetInnerHTML={{ __html: html }} />\n}\n\nfunction Ga4Script({ id }: { id: string }) {\n const safeId = JSON.stringify(id)\n const html = `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', ${safeId});`\n return (\n <>\n <script async src={`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`} />\n <script dangerouslySetInnerHTML={{ __html: html }} />\n </>\n )\n}\n\nfunction MetaPixelScript({ id }: { id: string }) {\n const safeId = JSON.stringify(id)\n const html = `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init', ${safeId});fbq('track', 'PageView');`\n return <script dangerouslySetInnerHTML={{ __html: html }} />\n}\n"],"mappings":";AA6CI,mBACyB,KADzB;AALG,SAAS,gBAAgB,EAAE,QAAQ,kBAAkB,MAAM,GAAyB;AACzF,QAAM,EAAE,mBAAmB,oBAAoB,YAAY,IAAI;AAC/D,QAAM,YAAY,CAAC,CAAC,sBAAsB,mBAAmB,CAAC;AAE9D,SACE,iCACG;AAAA,0BAAsB,oBAAC,iBAAc,IAAI,oBAAoB;AAAA,IAC7D,aAAa,qBAAqB,oBAAC,aAAU,IAAI,mBAAmB;AAAA,IACpE,eAAe,oBAAC,mBAAgB,IAAI,aAAa;AAAA,KACpD;AAEJ;AAcO,SAAS,iBAAiB,EAAE,OAAO,GAA+B;AACvE,QAAM,EAAE,oBAAoB,YAAY,IAAI;AAC5C,SACE,iCACG;AAAA,0BACC,oBAAC,cACC;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,+CAA+C,mBAAmB,kBAAkB,CAAC;AAAA,QAC1F,QAAO;AAAA,QACP,OAAM;AAAA,QACN,OAAO,EAAE,SAAS,QAAQ,YAAY,SAAS;AAAA;AAAA,IACjD,GACF;AAAA,IAED,eACC,oBAAC,cACC;AAAA,MAAC;AAAA;AAAA,QACC,QAAO;AAAA,QACP,OAAM;AAAA,QACN,OAAO,EAAE,SAAS,OAAO;AAAA,QACzB,KAAI;AAAA,QACJ,KAAK,kCAAkC,mBAAmB,WAAW,CAAC;AAAA;AAAA,IACxE,GACF;AAAA,KAEJ;AAEJ;AAEA,SAAS,cAAc,EAAE,GAAG,GAAmB;AAC7C,QAAM,UAAU,KAAK,UAAU,EAAE;AACjC,QAAM,OAAO,sUAAsU,OAAO;AAC1V,SAAO,oBAAC,YAAO,yBAAyB,EAAE,QAAQ,KAAK,GAAG;AAC5D;AAEA,SAAS,UAAU,EAAE,GAAG,GAAmB;AACzC,QAAM,SAAS,KAAK,UAAU,EAAE;AAChC,QAAM,OAAO,8HAA8H,MAAM;AACjJ,SACE,iCACE;AAAA,wBAAC,YAAO,OAAK,MAAC,KAAK,+CAA+C,mBAAmB,EAAE,CAAC,IAAI;AAAA,IAC5F,oBAAC,YAAO,yBAAyB,EAAE,QAAQ,KAAK,GAAG;AAAA,KACrD;AAEJ;AAEA,SAAS,gBAAgB,EAAE,GAAG,GAAmB;AAC/C,QAAM,SAAS,KAAK,UAAU,EAAE;AAChC,QAAM,OAAO,uYAAuY,MAAM;AAC1Z,SAAO,oBAAC,YAAO,yBAAyB,EAAE,QAAQ,KAAK,GAAG;AAC5D;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@distinctagency/cms-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
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",
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
"types": "./dist/client.d.ts",
|
|
19
19
|
"import": "./dist/client.mjs",
|
|
20
20
|
"require": "./dist/client.js"
|
|
21
|
+
},
|
|
22
|
+
"./tracking-scripts": {
|
|
23
|
+
"types": "./dist/tracking-scripts.d.ts",
|
|
24
|
+
"import": "./dist/tracking-scripts.mjs",
|
|
25
|
+
"require": "./dist/tracking-scripts.js"
|
|
21
26
|
}
|
|
22
27
|
},
|
|
23
28
|
"peerDependencies": {
|