fragment-headless-sdk 1.0.4 → 1.0.6

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.
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { IAnnouncementContent } from "../../types";
3
- export default function AnnouncementButton({ content, }: {
3
+ export default function AnnouncementButton({ content, buttonHref, }: {
4
4
  content: IAnnouncementContent;
5
- }): React.JSX.Element;
5
+ buttonHref?: string;
6
+ }): React.JSX.Element | null;
@@ -1,21 +1,26 @@
1
1
  import React from "react";
2
2
  import { ButtonType } from "../../constants";
3
- export default function AnnouncementButton({ content, }) {
4
- return (React.createElement("a", { href: content.buttonLink || "#", className: "whitespace-nowrap rounded-md px-3 py-2 font-semibold text-sm no-underline text-white hover:cursor-pointer hover:opacity-70 py-2", style: {
5
- ...(content.buttonType === ButtonType.Text
6
- ? {
7
- textDecoration: "underline",
8
- color: content.textColor,
9
- }
10
- : {
11
- backgroundColor: content.buttonColor,
12
- color: content.buttonTextColor,
13
- }),
14
- }, ...(content.buttonLink
3
+ export default function AnnouncementButton({ content, buttonHref, }) {
4
+ // Don’t render if no button or explicitly None
5
+ if (!content?.buttonText || content.buttonType === ButtonType.None)
6
+ return null;
7
+ // If we weren’t given a usable href, don’t render a broken link
8
+ if (!buttonHref)
9
+ return null;
10
+ // Decide if link should open in a new tab.
11
+ // If you already have a boolean like `content.buttonLink` meaning "open in new tab",
12
+ // keep using it; otherwise you can add one later.
13
+ const openInNewTab = Boolean(content.buttonLink);
14
+ const style = content.buttonType === ButtonType.Text
15
+ ? {
16
+ textDecoration: "underline",
17
+ color: content.textColor,
18
+ }
19
+ : {
20
+ backgroundColor: content.buttonColor,
21
+ color: content.buttonTextColor,
22
+ };
23
+ return (React.createElement("a", { href: buttonHref, className: "whitespace-nowrap rounded-md px-3 py-2 text-sm font-semibold no-underline hover:cursor-pointer hover:opacity-70", style: style, ...(openInNewTab
15
24
  ? { target: "_blank", rel: "noopener noreferrer" }
16
- : {}), onClick: (e) => {
17
- if (!content.buttonLink) {
18
- e.preventDefault();
19
- }
20
- } }, content.buttonText));
25
+ : {}), "aria-label": content.buttonText }, content.buttonText));
21
26
  }
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  import { AnnouncementType } from "../../constants";
3
3
  import { IAnnouncementContent } from "../../types";
4
- export default function ({ content, type, handleClose, }: {
4
+ export default function Announcement({ content, type, handleClose, }: {
5
5
  content: IAnnouncementContent;
6
6
  type: AnnouncementType;
7
7
  handleClose: () => void;
8
- }): React.JSX.Element;
8
+ }): React.JSX.Element | null;
@@ -1,10 +1,22 @@
1
- import React from "react";
1
+ import React, { useEffect, useRef } from "react";
2
2
  import { AnnouncementType, ButtonType } from "../../constants";
3
+ import { buildClickUrl, fireImpressionWhenVisible } from "../../utils";
3
4
  import AnnouncementButton from "./AnnouncementButton";
4
5
  import { AnnouncementStyles } from "./AnnouncementStyles";
5
6
  import CountdownTimer from "./CountdownTimer";
6
- export default function ({ content, type, handleClose, }) {
7
- return (React.createElement("div", { className: "relative w-full", style: {
7
+ export default function Announcement({ content, type, handleClose, }) {
8
+ const ref = useRef(null);
9
+ useEffect(() => {
10
+ if (ref.current && content.impressionUrl) {
11
+ fireImpressionWhenVisible(ref.current, content.impressionUrl);
12
+ }
13
+ }, [content?.impressionUrl]);
14
+ const signedButtonHref = content?.buttonLink && content?.clickUrlBase
15
+ ? buildClickUrl(content.clickUrlBase, content.buttonLink)
16
+ : undefined;
17
+ if (!content)
18
+ return null;
19
+ return (React.createElement("div", { ref: ref, className: "relative w-full", style: {
8
20
  backgroundColor: content.bgColor,
9
21
  color: content.textColor,
10
22
  } },
@@ -16,13 +28,13 @@ export default function ({ content, type, handleClose, }) {
16
28
  React.createElement("div", { className: "inline-block max-w-none text-base", dangerouslySetInnerHTML: {
17
29
  __html: content.announcementHtml || "",
18
30
  } }))),
19
- content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content })))) : (React.createElement("div", { className: "flex flex-col md:flex-row items-center justify-center gap-2 md:gap-4 w-full" },
31
+ content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: signedButtonHref })))) : (React.createElement("div", { className: "flex flex-col md:flex-row items-center justify-center gap-2 md:gap-4 w-full" },
20
32
  React.createElement("div", { className: "max-w-none text-base font-semibold" },
21
33
  React.createElement("div", { dangerouslySetInnerHTML: {
22
34
  __html: content.announcementHtml || "",
23
35
  } })),
24
36
  type === AnnouncementType.Countdown ? (React.createElement(CountdownTimer, { content: content })) : (content.buttonText &&
25
- content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content }))))),
37
+ content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: signedButtonHref || content.buttonLink || "#" }))))),
26
38
  React.createElement("div", { onClick: handleClose, className: "absolute right-4 top-1/2 -translate-y-1/2 text-3xl leading-none cursor-pointer", style: {
27
39
  color: content.textColor || "#000",
28
40
  } }, "\u00D7"))));
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { IHeroContent } from "../../types";
3
- export default function DesktopHero({ content }: {
3
+ export default function DesktopHero({ buttonHref, content, }: {
4
+ buttonHref?: string;
4
5
  content: IHeroContent;
5
6
  }): React.JSX.Element;
@@ -1,12 +1,12 @@
1
1
  import React from "react";
2
- export default function DesktopHero({ content }) {
2
+ export default function DesktopHero({ buttonHref, content, }) {
3
3
  return (React.createElement("div", { className: "relative h-[400px] gap-4 w-full" },
4
4
  content?.imageUrl && (React.createElement("img", { src: content.imageUrl, alt: content.title || "Hero", className: "absolute inset-0 z-0 object-cover w-full h-full" })),
5
5
  React.createElement("div", { className: "relative z-10 mx-auto flex h-full max-w-screen-xl flex-col items-start justify-center px-10 text-left xl:px-4" },
6
6
  React.createElement("div", { className: "w-2/5" },
7
7
  content?.title && (React.createElement("h1", { className: "text-5xl font-bold leading-tight drop-shadow-xl", style: { color: content.titleColor || "#ffffff" } }, content.title)),
8
8
  content?.description && (React.createElement("div", { className: "mt-4 text-2xl drop-shadow-lg prose", style: { color: content.textColor || undefined }, dangerouslySetInnerHTML: { __html: content.description } })),
9
- content?.buttonLink && content?.buttonText && (React.createElement("a", { href: content.buttonLink, target: "_blank", rel: "noopener noreferrer", className: "no-underline" },
9
+ content?.buttonLink && content?.buttonText && (React.createElement("a", { href: buttonHref, target: "_blank", rel: "noopener noreferrer", className: "no-underline" },
10
10
  React.createElement("div", { className: "mt-6 rounded-md px-8 py-2 text-2xl font-semibold drop-shadow-lg transition-all duration-200 hover:bg-gray-800 inline-block", style: {
11
11
  color: content.buttonTextColor ?? undefined,
12
12
  backgroundColor: content.buttonColor ?? undefined,
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { IHeroContent } from "../../types";
3
- export default function MobileHero({ content }: {
3
+ export default function MobileHero({ buttonHref, content, }: {
4
+ buttonHref?: string;
4
5
  content: IHeroContent;
5
6
  }): React.JSX.Element;
@@ -1,11 +1,11 @@
1
1
  import React from "react";
2
- export default function MobileHero({ content }) {
2
+ export default function MobileHero({ buttonHref, content, }) {
3
3
  return (React.createElement("div", { className: "relative z-10 mx-auto gap-4 flex h-full max-w-screen-md flex-col items-center justify-center py-6 text-center" },
4
4
  content?.title && (React.createElement("h1", { className: "text-3xl font-bold drop-shadow-xl px-4", style: { color: content.titleColor || undefined } }, content.title)),
5
5
  (content?.mobileImageUrl || content?.imageUrl) && (React.createElement("div", { className: "w-full" },
6
6
  React.createElement("img", { src: content.mobileImageUrl || content.imageUrl || "", alt: content.title || "Hero", className: "h-full w-full object-cover" }))),
7
7
  content?.description && (React.createElement("div", { className: "px-4 text-2xl drop-shadow-lg prose", style: { color: content.textColor || undefined }, dangerouslySetInnerHTML: { __html: content.description } })),
8
- content?.buttonLink && content?.buttonText && (React.createElement("a", { href: content.buttonLink, target: "_blank", rel: "noopener noreferrer", className: "no-underline" },
8
+ content?.buttonLink && content?.buttonText && (React.createElement("a", { href: buttonHref, target: "_blank", rel: "noopener noreferrer", className: "no-underline" },
9
9
  React.createElement("div", { className: "mb-2 rounded-md px-6 py-2 text-lg font-semibold drop-shadow-lg transition-all duration-200 hover:bg-gray-800", style: {
10
10
  color: content.buttonTextColor ?? undefined,
11
11
  backgroundColor: content.buttonColor ?? undefined,
@@ -1,12 +1,22 @@
1
- import React from "react";
1
+ import React, { useEffect, useRef } from "react";
2
+ import { buildClickUrl, fireImpressionWhenVisible } from "../../utils";
2
3
  import DesktopHero from "./DesktopHero";
3
4
  import MobileHero from "./MobileHero";
4
5
  export default function Hero({ content }) {
6
+ const ref = useRef(null);
7
+ useEffect(() => {
8
+ if (ref.current && content.impressionUrl) {
9
+ fireImpressionWhenVisible(ref.current, content.impressionUrl);
10
+ }
11
+ }, [content?.impressionUrl]);
12
+ const signedButtonHref = content?.buttonLink && content?.clickUrlBase
13
+ ? buildClickUrl(content.clickUrlBase, content.buttonLink)
14
+ : undefined;
5
15
  if (!content)
6
16
  return null;
7
- return (React.createElement("div", { className: "bg-black" },
17
+ return (React.createElement("div", { className: "bg-black", ref: ref },
8
18
  React.createElement("div", { className: "hidden lg:block" },
9
- React.createElement(DesktopHero, { content: content })),
19
+ React.createElement(DesktopHero, { content: content, buttonHref: signedButtonHref })),
10
20
  React.createElement("div", { className: "block lg:hidden" },
11
- React.createElement(MobileHero, { content: content }))));
21
+ React.createElement(MobileHero, { content: content, buttonHref: signedButtonHref }))));
12
22
  }
@@ -15,6 +15,8 @@ export interface IAnnouncementContent {
15
15
  counterEndDate?: string;
16
16
  counterBgColor?: string;
17
17
  counterDigitColor?: string;
18
+ impressionUrl: string;
19
+ clickUrlBase: string;
18
20
  }
19
21
  export interface IAnnouncement {
20
22
  id: string;
@@ -16,6 +16,8 @@ export interface IHeroContent {
16
16
  imageUrl: string;
17
17
  mobileImageUrl: string;
18
18
  videoUrl?: string;
19
+ impressionUrl: string;
20
+ clickUrlBase: string;
19
21
  }
20
22
  export interface IHero {
21
23
  id: string;
@@ -0,0 +1,30 @@
1
+ import { ResourceType } from "./fetch-resource";
2
+ /**
3
+ * Cache invalidation utilities for Next.js App Router
4
+ */
5
+ /**
6
+ * Revalidates Fragment cache tags in Next.js
7
+ * @param tags - Specific cache tags to revalidate (optional)
8
+ */
9
+ export declare function revalidateFragmentCache(tags?: string[]): Promise<void>;
10
+ /**
11
+ * Revalidates cache for a specific resource type
12
+ * @param type - The resource type to revalidate
13
+ */
14
+ export declare function revalidateResourceType(type: ResourceType): Promise<void>;
15
+ /**
16
+ * Revalidates all Fragment caches
17
+ */
18
+ export declare function revalidateAllFragmentCaches(): Promise<void>;
19
+ /**
20
+ * Creates a cache tag for a resource type
21
+ * @param type - The resource type
22
+ * @returns The cache tag string
23
+ */
24
+ export declare function createCacheTag(type: ResourceType): string;
25
+ /**
26
+ * Creates multiple cache tags for resource types
27
+ * @param types - Array of resource types
28
+ * @returns Array of cache tag strings
29
+ */
30
+ export declare function createCacheTags(types: ResourceType[]): string[];
@@ -0,0 +1,75 @@
1
+ import { ResourceType } from "./fetch-resource";
2
+ /**
3
+ * Cache invalidation utilities for Next.js App Router
4
+ */
5
+ /**
6
+ * Revalidates Fragment cache tags in Next.js
7
+ * @param tags - Specific cache tags to revalidate (optional)
8
+ */
9
+ export async function revalidateFragmentCache(tags) {
10
+ // Only works in Next.js server environment
11
+ if (typeof window !== "undefined") {
12
+ console.warn("revalidateFragmentCache can only be called on the server side");
13
+ return;
14
+ }
15
+ try {
16
+ // Dynamic import to avoid issues in non-Next.js environments
17
+ // @ts-ignore - next/cache may not be available in all environments
18
+ const { revalidateTag } = await import("next/cache");
19
+ if (tags && tags.length > 0) {
20
+ // Revalidate specific tags
21
+ tags.forEach((tag) => {
22
+ try {
23
+ revalidateTag(tag);
24
+ }
25
+ catch (err) {
26
+ console.warn(`Failed to revalidate tag ${tag}:`, err);
27
+ }
28
+ });
29
+ }
30
+ else {
31
+ // Revalidate all Fragment resource types
32
+ Object.values(ResourceType).forEach((type) => {
33
+ try {
34
+ revalidateTag(`fragment-${type}`);
35
+ }
36
+ catch (err) {
37
+ console.warn(`Failed to revalidate fragment-${type}:`, err);
38
+ }
39
+ });
40
+ }
41
+ }
42
+ catch (err) {
43
+ // Silently fail if not in Next.js environment
44
+ console.debug("Cache revalidation not available (not in Next.js environment)");
45
+ }
46
+ }
47
+ /**
48
+ * Revalidates cache for a specific resource type
49
+ * @param type - The resource type to revalidate
50
+ */
51
+ export async function revalidateResourceType(type) {
52
+ return revalidateFragmentCache([`fragment-${type}`]);
53
+ }
54
+ /**
55
+ * Revalidates all Fragment caches
56
+ */
57
+ export async function revalidateAllFragmentCaches() {
58
+ return revalidateFragmentCache();
59
+ }
60
+ /**
61
+ * Creates a cache tag for a resource type
62
+ * @param type - The resource type
63
+ * @returns The cache tag string
64
+ */
65
+ export function createCacheTag(type) {
66
+ return `fragment-${type}`;
67
+ }
68
+ /**
69
+ * Creates multiple cache tags for resource types
70
+ * @param types - Array of resource types
71
+ * @returns Array of cache tag strings
72
+ */
73
+ export function createCacheTags(types) {
74
+ return types.map((type) => createCacheTag(type));
75
+ }
@@ -2,10 +2,30 @@ export declare enum ResourceType {
2
2
  HeroBanners = "hero-banners",
3
3
  Announcements = "announcements"
4
4
  }
5
+ export type ListParams = {
6
+ status?: "enabled" | "disabled";
7
+ page?: number;
8
+ limit?: number;
9
+ search?: string;
10
+ pageFilter?: string;
11
+ };
12
+ export type CacheOptions = {
13
+ /** Request cache mode (default: 'no-store' for fresh data) */
14
+ cache?: RequestCache;
15
+ /** Next.js revalidation time in seconds (default: 0 for always fresh) */
16
+ revalidate?: number | false;
17
+ /** Next.js cache tags for selective invalidation */
18
+ tags?: string[];
19
+ };
5
20
  type FetchResourceParams = {
6
21
  baseUrl: string;
7
22
  apiKey: string;
8
23
  type: ResourceType;
24
+ params?: ListParams;
25
+ fetchImpl?: typeof fetch;
26
+ /** Cache configuration (defaults to no caching for fresh data) */
27
+ cacheOptions?: CacheOptions;
9
28
  };
10
- export declare function fetchResource<T>({ baseUrl, apiKey, type, }: FetchResourceParams): Promise<T[]>;
29
+ /** Lists resources with optional filters (parity with client.list) */
30
+ export declare function fetchResource<T>({ baseUrl, apiKey, type, params, fetchImpl, cacheOptions, }: FetchResourceParams): Promise<T[]>;
11
31
  export {};
@@ -3,39 +3,99 @@ export var ResourceType;
3
3
  ResourceType["HeroBanners"] = "hero-banners";
4
4
  ResourceType["Announcements"] = "announcements";
5
5
  })(ResourceType || (ResourceType = {}));
6
- export async function fetchResource({ baseUrl, apiKey, type, }) {
6
+ /**
7
+ * Detects if running in Next.js environment
8
+ */
9
+ function isNextJSEnvironment() {
10
+ return (typeof globalThis !== "undefined" &&
11
+ ("__NEXT_DATA__" in globalThis ||
12
+ process?.env?.NEXT_RUNTIME !== undefined ||
13
+ (typeof window !== "undefined" &&
14
+ window.__NEXT_DATA__ !== undefined)));
15
+ }
16
+ /**
17
+ * Gets default cache configuration optimized for Next.js
18
+ */
19
+ function getDefaultCacheConfig(type) {
20
+ const isNextJS = isNextJSEnvironment();
21
+ const isDev = process?.env?.NODE_ENV === "development";
22
+ if (isNextJS && !isDev) {
23
+ // In Next.js production, default to no caching for fresh data
24
+ return {
25
+ cache: "no-store",
26
+ revalidate: 0,
27
+ tags: [`fragment-${type}`],
28
+ };
29
+ }
30
+ // In development or non-Next.js environments, use default caching
31
+ return {
32
+ cache: "default",
33
+ };
34
+ }
35
+ /** Lists resources with optional filters (parity with client.list) */
36
+ export async function fetchResource({ baseUrl, apiKey, type, params = {}, fetchImpl, cacheOptions, }) {
7
37
  if (!baseUrl || !apiKey) {
8
38
  console.warn("❌ Missing EXTERNAL_API_URL or FRAGMENT_API_KEY");
9
39
  return [];
10
40
  }
11
41
  try {
12
- // Build the endpoint URL for the specific resource type
13
- const endpoint = `${baseUrl}/api/v1/${type}`;
14
- const res = await fetch(endpoint, {
42
+ const f = fetchImpl ?? fetch;
43
+ const base = baseUrl.replace(/\/+$/, "");
44
+ const url = new URL(`/api/v1/${type}`, base);
45
+ // Apply filters/pagination (clamped)
46
+ const page = Math.max(1, params.page ?? 1);
47
+ const limit = Math.min(100, Math.max(1, params.limit ?? 25));
48
+ url.searchParams.set("pageNum", String(page));
49
+ url.searchParams.set("limit", String(limit));
50
+ if (params.status)
51
+ url.searchParams.set("status", params.status);
52
+ if (params.pageFilter)
53
+ url.searchParams.set("page", params.pageFilter);
54
+ if (params.search)
55
+ url.searchParams.set("search", params.search);
56
+ // Merge default cache config with user-provided options
57
+ const finalCacheOptions = {
58
+ ...getDefaultCacheConfig(type),
59
+ ...cacheOptions,
60
+ };
61
+ // Build fetch options with cache configuration
62
+ const fetchOptions = {
15
63
  method: "GET",
16
64
  headers: {
17
65
  Authorization: `Bearer ${apiKey}`,
18
66
  "Content-Type": "application/json",
67
+ // Add cache-busting headers for better cache control
68
+ "Cache-Control": "no-cache, no-store, must-revalidate",
69
+ Pragma: "no-cache",
19
70
  },
20
- });
71
+ cache: finalCacheOptions.cache,
72
+ };
73
+ // Add Next.js specific options if available
74
+ if (isNextJSEnvironment() && finalCacheOptions.revalidate !== undefined) {
75
+ fetchOptions.next = {
76
+ revalidate: finalCacheOptions.revalidate,
77
+ ...(finalCacheOptions.tags && { tags: finalCacheOptions.tags }),
78
+ };
79
+ }
80
+ const res = await f(url.toString(), fetchOptions);
21
81
  if (!res.ok) {
22
82
  throw new Error(`HTTP ${res.status}: ${res.statusText}`);
23
83
  }
24
- const json = await res.json();
25
- if (json.status === "success") {
26
- // The API returns { status: "success", data: { items: [...], total, page, limit } }
27
- if (json.data?.items && Array.isArray(json.data.items)) {
28
- return json.data.items;
29
- }
30
- // Fallback for direct array response
31
- if (Array.isArray(json.data)) {
32
- return json.data;
33
- }
84
+ const json = (await res.json());
85
+ if (json.status === "error") {
86
+ console.error(`❌ Failed to load ${type}:`, json.message);
87
+ return [];
34
88
  }
35
- console.error(`❌ Failed to load ${type}:`, json.message || "Unknown error");
89
+ const data = json.data;
90
+ if (Array.isArray(data))
91
+ return data;
92
+ if (Array.isArray(data?.items))
93
+ return data.items;
94
+ // Fallback: unknown shape
95
+ return [];
36
96
  }
37
97
  catch (err) {
38
98
  console.error(`❌ Fetch error for ${type}:`, err);
99
+ return [];
39
100
  }
40
- return [];
41
101
  }
@@ -1 +1,3 @@
1
+ export * from "./cache";
1
2
  export * from "./fetch-resource";
3
+ export * from "./metrics";
@@ -1 +1,3 @@
1
+ export * from "./cache";
1
2
  export * from "./fetch-resource";
3
+ export * from "./metrics";
@@ -0,0 +1,3 @@
1
+ export declare function toBase64Url(input: string): string;
2
+ export declare function buildClickUrl(clickUrlBase: string, targetHref: string): string;
3
+ export declare function fireImpressionWhenVisible(el: HTMLElement, pixelUrl: string): void;
@@ -0,0 +1,56 @@
1
+ // --- Unicode-safe Base64URL ---
2
+ export function toBase64Url(input) {
3
+ // Handles emojis & non-ASCII reliably:
4
+ const b64 = btoa(encodeURIComponent(input).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))));
5
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6
+ }
7
+ // --- Robust query appender ---
8
+ function appendQuery(url, key, value) {
9
+ const sep = url.includes("?") ? "&" : "?";
10
+ return `${url}${sep}${key}=${value}`;
11
+ }
12
+ // Build the final redirect URL the CTA should use
13
+ export function buildClickUrl(clickUrlBase, targetHref) {
14
+ const u = encodeURIComponent(toBase64Url(targetHref));
15
+ return appendQuery(clickUrlBase, "u", u);
16
+ }
17
+ // --- View tracking (once per element) ---
18
+ const seenEls = typeof WeakSet !== "undefined" ? new WeakSet() : null;
19
+ export function fireImpressionWhenVisible(el, pixelUrl) {
20
+ if (typeof window === "undefined")
21
+ return; // SSR guard
22
+ if (!el || !pixelUrl)
23
+ return;
24
+ if (seenEls && seenEls.has(el))
25
+ return; // de-dupe by element
26
+ let fired = false;
27
+ const img = new Image();
28
+ const fire = () => {
29
+ if (fired)
30
+ return;
31
+ fired = true;
32
+ if (seenEls)
33
+ seenEls.add(el);
34
+ img.referrerPolicy = "strict-origin-when-cross-origin";
35
+ img.src = pixelUrl; // GET to your /api/v1/metrics/i?e=...&sig=...
36
+ };
37
+ // Fallback if IntersectionObserver is missing
38
+ const fallback = () => {
39
+ // delay a tick to avoid firing during hidden render
40
+ setTimeout(fire, 0);
41
+ };
42
+ if (!("IntersectionObserver" in window)) {
43
+ fallback();
44
+ return;
45
+ }
46
+ const io = new IntersectionObserver((entries) => {
47
+ for (const e of entries) {
48
+ if (e.isIntersecting && e.intersectionRatio >= 0.3) {
49
+ fire();
50
+ io.disconnect();
51
+ break;
52
+ }
53
+ }
54
+ }, { threshold: [0, 0.3] });
55
+ io.observe(el);
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fragment-headless-sdk",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -20,6 +20,7 @@
20
20
  "react": "^19.1.0"
21
21
  },
22
22
  "devDependencies": {
23
+ "@types/node": "^24.7.2",
23
24
  "@types/react": "^19.1.2",
24
25
  "typescript": "^5.8.3"
25
26
  }
@@ -1,41 +0,0 @@
1
- export declare enum HeroType {
2
- Static = "static",
3
- Video = "video"
4
- }
5
- export declare const heroTypes: {
6
- label: string;
7
- value: HeroType;
8
- }[];
9
- export declare enum HeroStatus {
10
- Enabled = "enabled",
11
- Disabled = "disabled"
12
- }
13
- export type ShopPage = {
14
- id: string;
15
- title: string;
16
- handle: string;
17
- };
18
- export interface HeroContent {
19
- title: string;
20
- titleColor: string;
21
- description: string;
22
- textColor: string;
23
- buttonText: string;
24
- buttonLink: string;
25
- buttonColor: string;
26
- buttonTextColor: string;
27
- imageUrl: string;
28
- mobileImageUrl: string;
29
- videoUrl?: string;
30
- }
31
- export interface HeroSection {
32
- id: string;
33
- shop: string;
34
- name: string;
35
- type: HeroType;
36
- page: ShopPage["handle"];
37
- status: HeroStatus;
38
- content: HeroContent | null;
39
- created_at: string;
40
- updated_at: string;
41
- }
@@ -1,2 +0,0 @@
1
- export type ResourceType = "hero_section" | "banner";
2
- export declare function fetchResource<T>(baseUrl: string, shop: string, token: string, type: ResourceType): Promise<T[]>;
@@ -1,28 +0,0 @@
1
- export async function fetchResource(baseUrl, shop, token, type) {
2
- if (!baseUrl || !shop || !token) {
3
- console.warn("❌ Missing EXTERNAL_API_URL, SHOP_DOMAIN, or EXTERNAL_API_KEY");
4
- return [];
5
- }
6
- try {
7
- const res = await fetch(`${baseUrl}/?type=${type}&shop=${shop}`, {
8
- method: "GET",
9
- headers: {
10
- Authorization: `Bearer ${token}`,
11
- },
12
- });
13
- const json = await res.json();
14
- if (json.status === "success") {
15
- if (Array.isArray(json.data)) {
16
- return json.data;
17
- }
18
- else if (Array.isArray(json.data?.[type])) {
19
- return json.data[type];
20
- }
21
- }
22
- console.error(`❌ Failed to load ${type}:`, json.message);
23
- }
24
- catch (err) {
25
- console.error(`❌ Fetch error for ${type}:`, err);
26
- }
27
- return [];
28
- }