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.
- package/dist/components/Announcement/AnnouncementButton.d.ts +3 -2
- package/dist/components/Announcement/AnnouncementButton.js +22 -17
- package/dist/components/Announcement/index.d.ts +2 -2
- package/dist/components/Announcement/index.js +17 -5
- package/dist/components/Hero/DesktopHero.d.ts +2 -1
- package/dist/components/Hero/DesktopHero.js +2 -2
- package/dist/components/Hero/MobileHero.d.ts +2 -1
- package/dist/components/Hero/MobileHero.js +2 -2
- package/dist/components/Hero/index.js +14 -4
- package/dist/types/announcement.d.ts +2 -0
- package/dist/types/hero.d.ts +2 -0
- package/dist/utils/cache.d.ts +30 -0
- package/dist/utils/cache.js +75 -0
- package/dist/utils/fetch-resource.d.ts +21 -1
- package/dist/utils/fetch-resource.js +77 -17
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/metrics.d.ts +3 -0
- package/dist/utils/metrics.js +56 -0
- package/package.json +2 -1
- package/dist/types/hero-section.d.ts +0 -41
- package/dist/utils/fetchResource.d.ts +0 -2
- package/dist/utils/fetchResource.js +0 -28
|
@@ -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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
: {}),
|
|
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
|
-
|
|
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,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:
|
|
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,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:
|
|
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
|
}
|
package/dist/types/hero.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
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 === "
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -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.
|
|
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,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
|
-
}
|