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.
Files changed (39) hide show
  1. package/dist/components/Banner/BannerButton.d.ts +5 -0
  2. package/dist/components/Banner/BannerButton.js +21 -0
  3. package/dist/components/Banner/BannerStyles.d.ts +3 -0
  4. package/dist/components/Banner/BannerStyles.js +33 -0
  5. package/dist/components/Banner/CountdownTimer.d.ts +5 -0
  6. package/dist/components/Banner/CountdownTimer.js +42 -0
  7. package/dist/components/Banner/index.d.ts +8 -0
  8. package/dist/components/Banner/index.js +27 -0
  9. package/dist/components/Hero/DesktopHero.d.ts +5 -0
  10. package/dist/components/Hero/DesktopHero.js +14 -0
  11. package/dist/components/Hero/MobileHero.d.ts +5 -0
  12. package/dist/components/Hero/MobileHero.js +13 -0
  13. package/dist/components/Hero/index.d.ts +5 -0
  14. package/dist/components/Hero/index.js +12 -0
  15. package/dist/components/index.d.ts +2 -0
  16. package/dist/components/index.js +2 -0
  17. package/dist/constants/banner.d.ts +18 -0
  18. package/dist/constants/banner.js +22 -0
  19. package/dist/constants/hero.d.ts +12 -0
  20. package/dist/constants/hero.js +14 -0
  21. package/dist/constants/index.d.ts +2 -0
  22. package/dist/constants/index.js +2 -0
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.js +4 -0
  25. package/dist/types/banner.d.ts +28 -0
  26. package/dist/types/banner.js +6 -0
  27. package/dist/types/hero-section.d.ts +41 -0
  28. package/dist/types/hero.d.ts +30 -0
  29. package/dist/types/hero.js +1 -0
  30. package/dist/types/index.d.ts +2 -0
  31. package/dist/types/index.js +2 -0
  32. package/dist/utils/fetch-resource.d.ts +11 -0
  33. package/dist/utils/fetch-resource.js +41 -0
  34. package/dist/utils/fetchResource.d.ts +2 -0
  35. package/dist/utils/fetchResource.js +28 -0
  36. package/dist/utils/index.d.ts +1 -0
  37. package/dist/utils/index.js +1 -0
  38. package/package.json +26 -0
  39. package/readme.md +404 -0
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ import { IBannerContent } from "../../types";
3
+ export default function BannerButton({ content }: {
4
+ content: IBannerContent;
5
+ }): React.JSX.Element;
@@ -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,3 @@
1
+ export declare function BannerStyles({ seconds }: {
2
+ seconds?: number;
3
+ }): null;
@@ -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,5 @@
1
+ import React from "react";
2
+ import { IBannerContent } from "../../types";
3
+ export default function CountdownTimer({ content, }: {
4
+ content: IBannerContent;
5
+ }): React.JSX.Element;
@@ -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,5 @@
1
+ import React from "react";
2
+ import { IHeroContent } from "../../types";
3
+ export default function DesktopHero({ content }: {
4
+ content: IHeroContent;
5
+ }): React.JSX.Element;
@@ -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,5 @@
1
+ import React from "react";
2
+ import { IHeroContent } from "../../types";
3
+ export default function MobileHero({ content }: {
4
+ content: IHeroContent;
5
+ }): React.JSX.Element;
@@ -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,5 @@
1
+ import React from "react";
2
+ import { IHeroContent } from "../../types";
3
+ export default function Hero({ content }: {
4
+ content: IHeroContent;
5
+ }): React.JSX.Element | null;
@@ -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,2 @@
1
+ export { default as Banner } from "./Banner";
2
+ export { default as Hero } from "./Hero";
@@ -0,0 +1,2 @@
1
+ export { default as Banner } from "./Banner";
2
+ export { default as Hero } from "./Hero";
@@ -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,12 @@
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
+ }
@@ -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 = {}));
@@ -0,0 +1,2 @@
1
+ export * from "./banner";
2
+ export * from "./hero";
@@ -0,0 +1,2 @@
1
+ export * from "./banner";
2
+ export * from "./hero";
@@ -0,0 +1,4 @@
1
+ export * from "./components";
2
+ export * from "./constants";
3
+ export * from "./types";
4
+ export * from "./utils";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./components";
2
+ export * from "./constants";
3
+ export * from "./types";
4
+ export * from "./utils";
@@ -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,6 @@
1
+ import { ButtonType } from "../constants";
2
+ export const buttonTypes = [
3
+ { label: "None", value: ButtonType.None },
4
+ { label: "Default Button", value: ButtonType.Default },
5
+ { label: "Text Link", value: ButtonType.Text },
6
+ ];
@@ -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,2 @@
1
+ export * from "./banner";
2
+ export * from "./hero";
@@ -0,0 +1,2 @@
1
+ export * from "./banner";
2
+ export * from "./hero";
@@ -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,2 @@
1
+ export type ResourceType = "hero_section" | "banner";
2
+ export declare function fetchResource<T>(baseUrl: string, shop: string, token: string, type: ResourceType): Promise<T[]>;
@@ -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.