fragment-headless-sdk 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Banner/BannerButton.d.ts +5 -0
- package/dist/components/Banner/BannerButton.js +21 -0
- package/dist/components/Banner/BannerStyles.d.ts +3 -0
- package/dist/components/Banner/BannerStyles.js +33 -0
- package/dist/components/Banner/CountdownTimer.d.ts +5 -0
- package/dist/components/Banner/CountdownTimer.js +42 -0
- package/dist/components/Banner/index.d.ts +8 -0
- package/dist/components/Banner/index.js +27 -0
- package/dist/components/Hero/DesktopHero.d.ts +5 -0
- package/dist/components/Hero/DesktopHero.js +14 -0
- package/dist/components/Hero/MobileHero.d.ts +5 -0
- package/dist/components/Hero/MobileHero.js +13 -0
- package/dist/components/Hero/index.d.ts +5 -0
- package/dist/components/Hero/index.js +12 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/constants/banner.d.ts +18 -0
- package/dist/constants/banner.js +22 -0
- package/dist/constants/hero.d.ts +12 -0
- package/dist/constants/hero.js +14 -0
- package/dist/constants/index.d.ts +2 -0
- package/dist/constants/index.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/types/banner.d.ts +28 -0
- package/dist/types/banner.js +6 -0
- package/dist/types/hero-section.d.ts +41 -0
- package/dist/types/hero.d.ts +30 -0
- package/dist/types/hero.js +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/fetch-resource.d.ts +11 -0
- package/dist/utils/fetch-resource.js +41 -0
- package/dist/utils/fetchResource.d.ts +2 -0
- package/dist/utils/fetchResource.js +28 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +26 -0
- package/readme.md +404 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ButtonType } from "../../constants";
|
|
3
|
+
export default function BannerButton({ content }) {
|
|
4
|
+
return (React.createElement("a", { href: content.buttonLink || "#", className: "whitespace-nowrap rounded-md px-3 py-2 font-medium text-base no-underline text-white hover:cursor-pointer hover:opacity-70", 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
|
|
15
|
+
? { target: "_blank", rel: "noopener noreferrer" }
|
|
16
|
+
: {}), onClick: (e) => {
|
|
17
|
+
if (!content.buttonLink) {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
}
|
|
20
|
+
} }, content.buttonText));
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
export function BannerStyles({ seconds = 15 }) {
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const style = document.createElement("style");
|
|
6
|
+
style.id = "banner-styles"; // Ensure unique ID to avoid duplicates
|
|
7
|
+
style.textContent = `
|
|
8
|
+
@keyframes marquee {
|
|
9
|
+
0% {
|
|
10
|
+
transform: translateX(100%);
|
|
11
|
+
}
|
|
12
|
+
100% {
|
|
13
|
+
transform: translateX(-100%);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.animate-marquee {
|
|
18
|
+
animation: marquee ${seconds}s linear infinite;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.Polaris-Page {
|
|
22
|
+
padding-bottom: 4rem !important;
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
if (!document.getElementById("banner-styles")) {
|
|
26
|
+
document.head.appendChild(style);
|
|
27
|
+
}
|
|
28
|
+
return () => {
|
|
29
|
+
document.getElementById("banner-styles")?.remove();
|
|
30
|
+
};
|
|
31
|
+
}, [seconds]);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
export default function CountdownTimer({ content, }) {
|
|
3
|
+
const [timeLeft, setTimeLeft] = useState({
|
|
4
|
+
days: "00",
|
|
5
|
+
hours: "00",
|
|
6
|
+
minutes: "00",
|
|
7
|
+
seconds: "00",
|
|
8
|
+
});
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!content.counterEndDate)
|
|
11
|
+
return;
|
|
12
|
+
const updateTimer = () => {
|
|
13
|
+
const now = new Date().getTime();
|
|
14
|
+
const target = content.counterEndDate
|
|
15
|
+
? new Date(content.counterEndDate).getTime()
|
|
16
|
+
: 0;
|
|
17
|
+
const diff = Math.max(0, target - now);
|
|
18
|
+
const days = String(Math.floor(diff / (1000 * 60 * 60 * 24))).padStart(2, "0");
|
|
19
|
+
const hours = String(Math.floor((diff / (1000 * 60 * 60)) % 24)).padStart(2, "0");
|
|
20
|
+
const minutes = String(Math.floor((diff / (1000 * 60)) % 60)).padStart(2, "0");
|
|
21
|
+
const seconds = String(Math.floor((diff / 1000) % 60)).padStart(2, "0");
|
|
22
|
+
setTimeLeft({ days, hours, minutes, seconds });
|
|
23
|
+
};
|
|
24
|
+
updateTimer();
|
|
25
|
+
const interval = setInterval(updateTimer, 1000);
|
|
26
|
+
return () => clearInterval(interval);
|
|
27
|
+
}, [content.counterEndDate]);
|
|
28
|
+
const renderBlock = (value, label) => (React.createElement("div", { className: "flex flex-col items-center" },
|
|
29
|
+
React.createElement("div", { className: "flex gap-1" }, value.split("").map((digit, i) => (React.createElement("span", { key: i, className: "w-6 h-7 rounded bg-black text-white text-xl font-bold flex items-center justify-center", style: {
|
|
30
|
+
backgroundColor: content.counterBgColor || "#000000",
|
|
31
|
+
color: content.counterDigitColor || "#FFFFFF",
|
|
32
|
+
} }, digit)))),
|
|
33
|
+
React.createElement("span", { className: "mt-0.5 text-xs" }, label)));
|
|
34
|
+
return (React.createElement("div", { className: "flex items-center gap-1 bg-gray-100 rounded" },
|
|
35
|
+
renderBlock(timeLeft.days, "Days"),
|
|
36
|
+
React.createElement("span", { className: "text-xl font-semibold -mt-4" }, ":"),
|
|
37
|
+
renderBlock(timeLeft.hours, "Hours"),
|
|
38
|
+
React.createElement("span", { className: "text-xl font-semibold -mt-4" }, ":"),
|
|
39
|
+
renderBlock(timeLeft.minutes, "Minutes"),
|
|
40
|
+
React.createElement("span", { className: "text-xl font-semibold -mt-4" }, ":"),
|
|
41
|
+
renderBlock(timeLeft.seconds, "Seconds")));
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { BannerType } from "../../constants";
|
|
3
|
+
import { IBannerContent } from "../../types";
|
|
4
|
+
export default function ({ content, type, handleClose, }: {
|
|
5
|
+
content: IBannerContent;
|
|
6
|
+
type: BannerType;
|
|
7
|
+
handleClose: () => void;
|
|
8
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { BannerType, ButtonType } from "../../constants";
|
|
3
|
+
import BannerButton from "./BannerButton";
|
|
4
|
+
import { BannerStyles } from "./BannerStyles";
|
|
5
|
+
import CountdownTimer from "./CountdownTimer";
|
|
6
|
+
export default function ({ content, type, handleClose, }) {
|
|
7
|
+
return (React.createElement("div", { className: "relative w-full", style: {
|
|
8
|
+
backgroundColor: content.bgColor,
|
|
9
|
+
color: content.textColor,
|
|
10
|
+
} },
|
|
11
|
+
React.createElement(BannerStyles, null),
|
|
12
|
+
React.createElement("div", { className: "relative mx-auto flex max-w-screen-xl flex-col md:flex-row items-center justify-center gap-4 px-4 py-3 text-center md:text-left" },
|
|
13
|
+
type === BannerType.Marquee ? (React.createElement("div", { className: "flex w-full flex-col md:flex-row items-center justify-between gap-2 md:gap-4 overflow-hidden md:pr-8" },
|
|
14
|
+
React.createElement("div", { className: "w-full md:flex-1 overflow-hidden" },
|
|
15
|
+
React.createElement("div", { className: "whitespace-nowrap animate-marquee" },
|
|
16
|
+
React.createElement("div", { className: "inline-block max-w-none text-base", dangerouslySetInnerHTML: {
|
|
17
|
+
__html: content.bannerHtml || "",
|
|
18
|
+
} }))),
|
|
19
|
+
content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(BannerButton, { content: content })))) : (React.createElement("div", { className: "flex flex-col md:flex-row items-center justify-center gap-2 md:gap-4 w-full" },
|
|
20
|
+
React.createElement("div", { className: "max-w-none text-base font-semibold" },
|
|
21
|
+
React.createElement("div", { dangerouslySetInnerHTML: { __html: content.bannerHtml || "" } })),
|
|
22
|
+
type === BannerType.Countdown ? (React.createElement(CountdownTimer, { content: content })) : (content.buttonText &&
|
|
23
|
+
content.buttonType !== ButtonType.None && (React.createElement(BannerButton, { content: content }))))),
|
|
24
|
+
React.createElement("div", { onClick: handleClose, className: "absolute right-4 top-1/2 -translate-y-1/2 text-3xl leading-none cursor-pointer", style: {
|
|
25
|
+
color: content.textColor || "#000",
|
|
26
|
+
} }, "\u00D7"))));
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export default function DesktopHero({ content }) {
|
|
3
|
+
return (React.createElement("div", { className: "relative h-[400px] gap-4 w-full" },
|
|
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
|
+
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
|
+
React.createElement("div", { className: "w-2/5" },
|
|
7
|
+
content?.title && (React.createElement("h1", { className: "text-5xl font-bold leading-tight drop-shadow-xl", style: { color: content.titleColor || "#ffffff" } }, content.title)),
|
|
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" },
|
|
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
|
+
color: content.buttonTextColor ?? undefined,
|
|
12
|
+
backgroundColor: content.buttonColor ?? undefined,
|
|
13
|
+
} }, content.buttonText)))))));
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export default function MobileHero({ content }) {
|
|
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
|
+
content?.title && (React.createElement("h1", { className: "text-3xl font-bold drop-shadow-xl px-4", style: { color: content.titleColor || undefined } }, content.title)),
|
|
5
|
+
(content?.mobileImageUrl || content?.imageUrl) && (React.createElement("div", { className: "w-full" },
|
|
6
|
+
React.createElement("img", { src: content.mobileImageUrl || content.imageUrl || "", alt: content.title || "Hero", className: "h-full w-full object-cover" }))),
|
|
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" },
|
|
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
|
+
color: content.buttonTextColor ?? undefined,
|
|
11
|
+
backgroundColor: content.buttonColor ?? undefined,
|
|
12
|
+
} }, content.buttonText)))));
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import DesktopHero from "./DesktopHero";
|
|
3
|
+
import MobileHero from "./MobileHero";
|
|
4
|
+
export default function Hero({ content }) {
|
|
5
|
+
if (!content)
|
|
6
|
+
return null;
|
|
7
|
+
return (React.createElement("div", { className: "bg-black" },
|
|
8
|
+
React.createElement("div", { className: "hidden lg:block" },
|
|
9
|
+
React.createElement(DesktopHero, { content: content })),
|
|
10
|
+
React.createElement("div", { className: "block lg:hidden" },
|
|
11
|
+
React.createElement(MobileHero, { content: content }))));
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare enum BannerType {
|
|
2
|
+
Countdown = "countdown",
|
|
3
|
+
Announcement = "announcement",
|
|
4
|
+
Marquee = "marquee"
|
|
5
|
+
}
|
|
6
|
+
export declare const bannerTypes: {
|
|
7
|
+
label: string;
|
|
8
|
+
value: BannerType;
|
|
9
|
+
}[];
|
|
10
|
+
export declare enum BannerStatus {
|
|
11
|
+
Enabled = "enabled",
|
|
12
|
+
Disabled = "disabled"
|
|
13
|
+
}
|
|
14
|
+
export declare enum ButtonType {
|
|
15
|
+
None = "none",
|
|
16
|
+
Default = "default",
|
|
17
|
+
Text = "text"
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export var BannerType;
|
|
2
|
+
(function (BannerType) {
|
|
3
|
+
BannerType["Countdown"] = "countdown";
|
|
4
|
+
BannerType["Announcement"] = "announcement";
|
|
5
|
+
BannerType["Marquee"] = "marquee";
|
|
6
|
+
})(BannerType || (BannerType = {}));
|
|
7
|
+
export const bannerTypes = [
|
|
8
|
+
{ label: "Announcement Banner", value: BannerType.Announcement },
|
|
9
|
+
{ label: "Countdown Timer", value: BannerType.Countdown },
|
|
10
|
+
{ label: "Scrolling Text / Marquee", value: BannerType.Marquee },
|
|
11
|
+
];
|
|
12
|
+
export var BannerStatus;
|
|
13
|
+
(function (BannerStatus) {
|
|
14
|
+
BannerStatus["Enabled"] = "enabled";
|
|
15
|
+
BannerStatus["Disabled"] = "disabled";
|
|
16
|
+
})(BannerStatus || (BannerStatus = {}));
|
|
17
|
+
export var ButtonType;
|
|
18
|
+
(function (ButtonType) {
|
|
19
|
+
ButtonType["None"] = "none";
|
|
20
|
+
ButtonType["Default"] = "default";
|
|
21
|
+
ButtonType["Text"] = "text";
|
|
22
|
+
})(ButtonType || (ButtonType = {}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export var HeroType;
|
|
2
|
+
(function (HeroType) {
|
|
3
|
+
HeroType["Static"] = "static";
|
|
4
|
+
HeroType["Video"] = "video";
|
|
5
|
+
})(HeroType || (HeroType = {}));
|
|
6
|
+
export const heroTypes = [
|
|
7
|
+
{ label: "Static Image", value: HeroType.Static },
|
|
8
|
+
{ label: "Video Background", value: HeroType.Video },
|
|
9
|
+
];
|
|
10
|
+
export var HeroStatus;
|
|
11
|
+
(function (HeroStatus) {
|
|
12
|
+
HeroStatus["Enabled"] = "enabled";
|
|
13
|
+
HeroStatus["Disabled"] = "disabled";
|
|
14
|
+
})(HeroStatus || (HeroStatus = {}));
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BannerStatus, BannerType, ButtonType } from "../constants";
|
|
2
|
+
export declare const buttonTypes: {
|
|
3
|
+
label: string;
|
|
4
|
+
value: ButtonType;
|
|
5
|
+
}[];
|
|
6
|
+
export interface IBannerContent {
|
|
7
|
+
buttonType: ButtonType;
|
|
8
|
+
buttonText: string;
|
|
9
|
+
buttonLink: string;
|
|
10
|
+
bgColor: string;
|
|
11
|
+
bannerHtml: string;
|
|
12
|
+
buttonColor: string;
|
|
13
|
+
buttonTextColor: string;
|
|
14
|
+
textColor: string;
|
|
15
|
+
counterEndDate?: string;
|
|
16
|
+
counterBgColor?: string;
|
|
17
|
+
counterDigitColor?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface IBanner {
|
|
20
|
+
id: string;
|
|
21
|
+
shop: string;
|
|
22
|
+
type: BannerType;
|
|
23
|
+
name: string;
|
|
24
|
+
status: BannerStatus;
|
|
25
|
+
content: IBannerContent | null;
|
|
26
|
+
created_at: string;
|
|
27
|
+
updated_at: string;
|
|
28
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HeroStatus, HeroType } from "../constants";
|
|
2
|
+
export type ShopPage = {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
handle: string;
|
|
6
|
+
};
|
|
7
|
+
export interface IHeroContent {
|
|
8
|
+
title: string;
|
|
9
|
+
titleColor: string;
|
|
10
|
+
description: string;
|
|
11
|
+
textColor: string;
|
|
12
|
+
buttonText: string;
|
|
13
|
+
buttonLink: string;
|
|
14
|
+
buttonColor: string;
|
|
15
|
+
buttonTextColor: string;
|
|
16
|
+
imageUrl: string;
|
|
17
|
+
mobileImageUrl: string;
|
|
18
|
+
videoUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface IHero {
|
|
21
|
+
id: string;
|
|
22
|
+
shop: string;
|
|
23
|
+
name: string;
|
|
24
|
+
type: HeroType;
|
|
25
|
+
page: ShopPage["handle"];
|
|
26
|
+
status: HeroStatus;
|
|
27
|
+
content: IHeroContent | null;
|
|
28
|
+
created_at: string;
|
|
29
|
+
updated_at: string;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare enum ResourceType {
|
|
2
|
+
HeroSections = "hero-sections",
|
|
3
|
+
Banners = "banners"
|
|
4
|
+
}
|
|
5
|
+
type FetchResourceParams = {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
type: ResourceType;
|
|
9
|
+
};
|
|
10
|
+
export declare function fetchResource<T>({ baseUrl, apiKey, type, }: FetchResourceParams): Promise<T[]>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export var ResourceType;
|
|
2
|
+
(function (ResourceType) {
|
|
3
|
+
ResourceType["HeroSections"] = "hero-sections";
|
|
4
|
+
ResourceType["Banners"] = "banners";
|
|
5
|
+
})(ResourceType || (ResourceType = {}));
|
|
6
|
+
export async function fetchResource({ baseUrl, apiKey, type, }) {
|
|
7
|
+
if (!baseUrl || !apiKey) {
|
|
8
|
+
console.warn("❌ Missing EXTERNAL_API_URL or FRAGMENT_API_KEY");
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
// Build the endpoint URL for the specific resource type
|
|
13
|
+
const endpoint = `${baseUrl}/api/v1/${type}`;
|
|
14
|
+
const res = await fetch(endpoint, {
|
|
15
|
+
method: "GET",
|
|
16
|
+
headers: {
|
|
17
|
+
Authorization: `Bearer ${apiKey}`,
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
23
|
+
}
|
|
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
|
+
}
|
|
34
|
+
}
|
|
35
|
+
console.error(`❌ Failed to load ${type}:`, json.message || "Unknown error");
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
console.error(`❌ Fetch error for ${type}:`, err);
|
|
39
|
+
}
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./fetch-resource";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./fetch-resource";
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fragment-headless-sdk",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"react": "^19.1.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/react": "^19.1.2",
|
|
24
|
+
"typescript": "^5.8.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# fragment-headless-sdk
|
|
2
|
+
|
|
3
|
+
The official SDK for integrating with fragment-shopify CMS. Provides React components, TypeScript types, and utilities for rendering published sections in headless Shopify storefronts.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🏗️ Architecture
|
|
8
|
+
|
|
9
|
+
This package works as a **consumer library** that integrates with the **fragment-shopify app**:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Fragment-Shopify App (CMS) → API Endpoint → fragment-shopify-sdk (Consumer) → Your Headless Site
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- **Fragment-Shopify App**: Content management system for creating and publishing sections
|
|
16
|
+
- **fragment-shopify-sdk**: This SDK - consumes and renders the published content
|
|
17
|
+
- **Your App**: Next.js/React application that uses the SDK
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🔧 Features
|
|
22
|
+
|
|
23
|
+
- ✅ **Data Fetching**: Built-in utilities to fetch sections from fragment-shopify API
|
|
24
|
+
- ✅ **React Components**: Pre-built Hero and Banner components with responsive design
|
|
25
|
+
- ✅ **TypeScript Support**: Full type definitions for all components and data structures
|
|
26
|
+
- ✅ **Tailwind Integration**: Styled with Tailwind CSS for easy customization
|
|
27
|
+
- ✅ **Multiple Banner Types**: Standard, marquee, and countdown banner variants
|
|
28
|
+
- ✅ **Hero Sections**: Desktop/mobile responsive hero components with video support
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 📦 Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Using Yarn
|
|
36
|
+
yarn add fragment-headless-sdk
|
|
37
|
+
|
|
38
|
+
# Using npm
|
|
39
|
+
npm install fragment-headless-sdk
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🎨 Tailwind CSS Setup
|
|
45
|
+
|
|
46
|
+
Since this package uses Tailwind utility classes, your host app must be configured with Tailwind CSS.
|
|
47
|
+
|
|
48
|
+
### 1. Install Tailwind
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install -D tailwindcss postcss autoprefixer
|
|
52
|
+
npx tailwindcss init -p
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Update `tailwind.config.js`
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const path = require("path");
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
content: [
|
|
62
|
+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
63
|
+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
64
|
+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
65
|
+
path.join(__dirname, "node_modules/fragment-headless-sdk/dist/**/*.{js,ts,jsx,tsx}"),
|
|
66
|
+
],
|
|
67
|
+
theme: {
|
|
68
|
+
extend: {},
|
|
69
|
+
},
|
|
70
|
+
plugins: [],
|
|
71
|
+
corePlugins: {
|
|
72
|
+
preflight: false,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. Include Tailwind in your global CSS
|
|
78
|
+
|
|
79
|
+
```css
|
|
80
|
+
@tailwind base;
|
|
81
|
+
@tailwind components;
|
|
82
|
+
@tailwind utilities;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 🚀 Quick Start
|
|
88
|
+
|
|
89
|
+
### 1. Fetch Data from Fragment-Shopify
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { fetchResource, ResourceType } from "fragment-headless-sdk";
|
|
93
|
+
|
|
94
|
+
// Fetch hero sections
|
|
95
|
+
const heroes = await fetchResource({
|
|
96
|
+
baseUrl: process.env.EXTERNAL_API_URL, // Your fragment-shopify app URL
|
|
97
|
+
apiKey: process.env.FRAGMENT_API_KEY, // API key from fragment-shopify (format: "keyId:secret")
|
|
98
|
+
type: ResourceType.HeroSections,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Fetch banners
|
|
102
|
+
const banners = await fetchResource({
|
|
103
|
+
baseUrl: process.env.EXTERNAL_API_URL,
|
|
104
|
+
apiKey: process.env.FRAGMENT_API_KEY,
|
|
105
|
+
type: ResourceType.Banners,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 2. Render Components
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { Banner, Hero } from "fragment-headless-sdk";
|
|
113
|
+
import { BannerType } from "fragment-headless-sdk";
|
|
114
|
+
|
|
115
|
+
export default function Page() {
|
|
116
|
+
const [showBanner, setShowBanner] = useState(true);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
{/* Hero Section */}
|
|
121
|
+
<Hero content={heroes[0]?.content} />
|
|
122
|
+
|
|
123
|
+
{/* Banner */}
|
|
124
|
+
{showBanner && banners[0] && (
|
|
125
|
+
<Banner
|
|
126
|
+
content={banners[0].content}
|
|
127
|
+
type={banners[0].type as BannerType}
|
|
128
|
+
handleClose={() => setShowBanner(false)}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 📡 API Reference
|
|
139
|
+
|
|
140
|
+
### `fetchResource<T>(params)`
|
|
141
|
+
|
|
142
|
+
Fetches sections from your fragment-shopify app.
|
|
143
|
+
|
|
144
|
+
**Parameters:**
|
|
145
|
+
|
|
146
|
+
- `baseUrl: string` - URL of your fragment-shopify app
|
|
147
|
+
- `apiKey: string` - Fragment API key (format: `keyId:secret`)
|
|
148
|
+
- `type: ResourceType` - Type of resource to fetch
|
|
149
|
+
|
|
150
|
+
**ResourceType Options:**
|
|
151
|
+
|
|
152
|
+
- `ResourceType.HeroSections` - Fetch hero sections
|
|
153
|
+
- `ResourceType.Banners` - Fetch banner sections
|
|
154
|
+
|
|
155
|
+
**Returns:** `Promise<T[]>` - Array of fetched resources
|
|
156
|
+
|
|
157
|
+
**Example Response:**
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"status": "success",
|
|
162
|
+
"data": [
|
|
163
|
+
{
|
|
164
|
+
"id": "hero-1",
|
|
165
|
+
"shop": "your-shop.myshopify.com",
|
|
166
|
+
"name": "Homepage Hero",
|
|
167
|
+
"content": {
|
|
168
|
+
"title": "Welcome to Our Store",
|
|
169
|
+
"subtitle": "Discover amazing products",
|
|
170
|
+
"description": "Shop the latest collection...",
|
|
171
|
+
"backgroundImage": "https://...",
|
|
172
|
+
"buttons": [...]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 🧩 Components
|
|
182
|
+
|
|
183
|
+
### Hero Component
|
|
184
|
+
|
|
185
|
+
Responsive hero section with support for images, videos, and call-to-action buttons.
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { Hero } from "fragment-headless-sdk";
|
|
189
|
+
|
|
190
|
+
<Hero content={heroContent} />;
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Props:**
|
|
194
|
+
|
|
195
|
+
- `content: IHeroContent` - Hero configuration object
|
|
196
|
+
|
|
197
|
+
**IHeroContent Interface:**
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
interface IHeroContent {
|
|
201
|
+
title?: string;
|
|
202
|
+
subtitle?: string;
|
|
203
|
+
description?: string;
|
|
204
|
+
backgroundImage?: string;
|
|
205
|
+
videoUrl?: string;
|
|
206
|
+
buttons?: Array<{
|
|
207
|
+
text: string;
|
|
208
|
+
link: string;
|
|
209
|
+
style: "primary" | "secondary";
|
|
210
|
+
}>;
|
|
211
|
+
shopPage?: {
|
|
212
|
+
id: string;
|
|
213
|
+
title: string;
|
|
214
|
+
handle: string;
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Banner Component
|
|
220
|
+
|
|
221
|
+
Flexible banner component with multiple display types and countdown functionality.
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import { Banner, BannerType } from "fragment-headless-sdk";
|
|
225
|
+
|
|
226
|
+
<Banner
|
|
227
|
+
content={bannerContent}
|
|
228
|
+
type={BannerType.Standard}
|
|
229
|
+
handleClose={() => setBannerVisible(false)}
|
|
230
|
+
/>;
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Props:**
|
|
234
|
+
|
|
235
|
+
- `content: IBannerContent` - Banner configuration object
|
|
236
|
+
- `type: BannerType` - Display type (Standard, Marquee, Countdown)
|
|
237
|
+
- `handleClose: () => void` - Function called when banner is closed
|
|
238
|
+
|
|
239
|
+
**Banner Types:**
|
|
240
|
+
|
|
241
|
+
- `BannerType.Standard` - Regular banner with text and button
|
|
242
|
+
- `BannerType.Marquee` - Scrolling marquee banner
|
|
243
|
+
- `BannerType.Countdown` - Banner with countdown timer
|
|
244
|
+
|
|
245
|
+
**IBannerContent Interface:**
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
interface IBannerContent {
|
|
249
|
+
bannerHtml: string;
|
|
250
|
+
bgColor: string;
|
|
251
|
+
textColor: string;
|
|
252
|
+
buttonType: ButtonType;
|
|
253
|
+
buttonText: string;
|
|
254
|
+
buttonLink: string;
|
|
255
|
+
buttonColor: string;
|
|
256
|
+
buttonTextColor: string;
|
|
257
|
+
counterEndDate?: string;
|
|
258
|
+
counterBgColor?: string;
|
|
259
|
+
counterDigitColor?: string;
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 🔑 API Key Setup
|
|
266
|
+
|
|
267
|
+
Before using the SDK, you need to generate an API key from your fragment-shopify app:
|
|
268
|
+
|
|
269
|
+
### 1. Generate API Key
|
|
270
|
+
|
|
271
|
+
1. Install the Fragment app in your Shopify store
|
|
272
|
+
2. Navigate to the **Configuration** section within the app
|
|
273
|
+
3. Find the **"API Access"** section
|
|
274
|
+
4. Click **"Generate Key"** to create your API key
|
|
275
|
+
5. **Save the key securely** - it's only shown once!
|
|
276
|
+
|
|
277
|
+
### 2. API Key Format
|
|
278
|
+
|
|
279
|
+
The API key follows this format: `keyId:secret`
|
|
280
|
+
|
|
281
|
+
Example: `bh_a1b2c3d4e5f6:your-64-char-secret-here`
|
|
282
|
+
|
|
283
|
+
### 3. Key Management
|
|
284
|
+
|
|
285
|
+
- Only **one active key** per shop for security
|
|
286
|
+
- Keys **expire after 1 year** by default
|
|
287
|
+
- You can **regenerate keys** when needed (revokes the old key)
|
|
288
|
+
- Monitor key usage in the Configuration page
|
|
289
|
+
|
|
290
|
+
### ✅ Ready for Production
|
|
291
|
+
|
|
292
|
+
The external API is fully implemented! The fragment-shopify app now supports complete API key authentication with the v1 endpoints. Your SDK is ready to use in production.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## 🔧 Environment Variables
|
|
297
|
+
|
|
298
|
+
Add these environment variables to your `.env.local`:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Fragment-Shopify Integration
|
|
302
|
+
EXTERNAL_API_URL=https://your-fragment-app.vercel.app
|
|
303
|
+
FRAGMENT_API_KEY=bh_a1b2c3d4e5f6:your-64-char-secret
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## 🎨 Styling & Customization
|
|
309
|
+
|
|
310
|
+
### Tailwind CSS Classes
|
|
311
|
+
|
|
312
|
+
Components use Tailwind utility classes. You can customize styling by:
|
|
313
|
+
|
|
314
|
+
1. **Overriding default styles** with your own CSS classes
|
|
315
|
+
2. **Using Tailwind's configuration** to modify the design system
|
|
316
|
+
3. **Applying custom colors** through the content configuration objects
|
|
317
|
+
|
|
318
|
+
### Component Styling
|
|
319
|
+
|
|
320
|
+
**Hero Component:**
|
|
321
|
+
|
|
322
|
+
- Responsive design with `md:` breakpoints
|
|
323
|
+
- Background image/video support
|
|
324
|
+
- Customizable button styles through content props
|
|
325
|
+
|
|
326
|
+
**Banner Component:**
|
|
327
|
+
|
|
328
|
+
- Dynamic background and text colors via `content.bgColor` and `content.textColor`
|
|
329
|
+
- Marquee animation with `animate-marquee` class
|
|
330
|
+
- Countdown timer with customizable colors
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 🔄 Data Flow
|
|
335
|
+
|
|
336
|
+
```mermaid
|
|
337
|
+
graph TD
|
|
338
|
+
A[Fragment-Shopify App] --> B[API Endpoint]
|
|
339
|
+
B --> C[fetchResource()]
|
|
340
|
+
C --> D[Your React App]
|
|
341
|
+
D --> E[Hero/Banner Components]
|
|
342
|
+
E --> F[Rendered UI]
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
1. **Content Creation**: Editors create sections in fragment-shopify app
|
|
346
|
+
2. **Publishing**: Sections are published and available via API
|
|
347
|
+
3. **Data Fetching**: Your app calls `fetchResource()` to get section data
|
|
348
|
+
4. **Component Rendering**: Pass data to Hero/Banner components
|
|
349
|
+
5. **UI Display**: Components render responsive, styled sections
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## 🚨 Error Handling
|
|
354
|
+
|
|
355
|
+
The package includes built-in error handling:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// fetchResource automatically handles:
|
|
359
|
+
// - Missing environment variables (logs warning, returns [])
|
|
360
|
+
// - API errors (logs error, returns [])
|
|
361
|
+
// - Invalid response format (logs error, returns [])
|
|
362
|
+
|
|
363
|
+
const sections = await fetchResource({...});
|
|
364
|
+
// sections will always be an array, even on error
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Best Practices:**
|
|
368
|
+
|
|
369
|
+
- Always check if data exists before rendering: `{sections[0] && <Hero content={sections[0].content} />}`
|
|
370
|
+
- Implement fallback UI for when sections are unavailable
|
|
371
|
+
- Monitor console for API warnings and errors
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## 📦 Package Structure
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
fragment-headless-sdk/
|
|
379
|
+
├── src/
|
|
380
|
+
│ ├── components/ # React components
|
|
381
|
+
│ │ ├── Banner/ # Banner component variants
|
|
382
|
+
│ │ └── Hero/ # Hero component variants
|
|
383
|
+
│ ├── constants/ # Enums and constants
|
|
384
|
+
│ ├── types/ # TypeScript interfaces
|
|
385
|
+
│ └── utils/ # Utility functions
|
|
386
|
+
└── dist/ # Compiled output
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 🤝 Contributing
|
|
392
|
+
|
|
393
|
+
This package is designed to work specifically with the fragment-shopify app. When adding new section types:
|
|
394
|
+
|
|
395
|
+
1. Add the resource type to `ResourceType` enum
|
|
396
|
+
2. Create corresponding TypeScript interfaces
|
|
397
|
+
3. Build React components for rendering
|
|
398
|
+
4. Update this documentation
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## 📝 License
|
|
403
|
+
|
|
404
|
+
MIT License - See LICENSE file for details.
|