@foxpixel/react 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/context/FoxPixelContext.tsx
2
- import { createContext, useContext, useMemo } from "react";
2
+ import React, { createContext, useContext, useMemo } from "react";
3
3
 
4
4
  // src/client/http.ts
5
5
  import axios from "axios";
@@ -111,15 +111,21 @@ var FoxPixelHttpClient = class {
111
111
 
112
112
  // src/context/FoxPixelContext.tsx
113
113
  import { jsx } from "react/jsx-runtime";
114
+ if (!React || typeof React.useMemo !== "function") {
115
+ throw new Error(
116
+ '@foxpixel/react: React is not available. Ensure your app uses a single React instance and the SDK is not bundled with a different React. In Next.js use transpilePackages: ["@foxpixel/react"] and ensure react/react-dom resolve to one module (see next.config.js).'
117
+ );
118
+ }
114
119
  var FoxPixelContext = createContext(null);
115
- function FoxPixelProvider({ children, config = {} }) {
120
+ function FoxPixelProvider({ children, config = {}, queryClient }) {
116
121
  const client = useMemo(() => {
117
122
  return new FoxPixelHttpClient(config);
118
123
  }, [config.apiUrl, config.apiKey, config.tenantId]);
119
124
  const value = useMemo(() => ({
120
125
  client,
121
- config
122
- }), [client, config]);
126
+ config,
127
+ queryClient: queryClient ?? null
128
+ }), [client, config, queryClient]);
123
129
  return /* @__PURE__ */ jsx(FoxPixelContext.Provider, { value, children });
124
130
  }
125
131
  function useFoxPixelContext() {
@@ -346,12 +352,33 @@ function withAuth(Component, options = {}) {
346
352
  }
347
353
 
348
354
  // src/components/Editable.tsx
349
- import { createElement, useCallback as useCallback3 } from "react";
355
+ import { createElement, useCallback as useCallback3, useState as useState7, useEffect as useEffect7 } from "react";
350
356
 
351
357
  // src/hooks/useEditMode.ts
352
358
  import { useEffect as useEffect5, useState as useState5, useCallback as useCallback2 } from "react";
353
- import { useQueryClient } from "@tanstack/react-query";
354
359
  var SITE_CONTENT_QUERY_KEY = "siteContent";
360
+ var contentKeysOnPage = /* @__PURE__ */ new Set();
361
+ var flushTimer = null;
362
+ function scheduleFlushContentKeys() {
363
+ if (flushTimer) return;
364
+ flushTimer = setTimeout(() => {
365
+ flushTimer = null;
366
+ if (typeof window !== "undefined" && window.parent !== window) {
367
+ window.parent.postMessage(
368
+ { type: "FOXPIXEL_READY", payload: { contentKeys: Array.from(contentKeysOnPage) } },
369
+ "*"
370
+ );
371
+ }
372
+ }, 150);
373
+ }
374
+ function registerContentKey(key) {
375
+ contentKeysOnPage.add(key);
376
+ scheduleFlushContentKeys();
377
+ }
378
+ function unregisterContentKey(key) {
379
+ contentKeysOnPage.delete(key);
380
+ scheduleFlushContentKeys();
381
+ }
355
382
  function useEditMode() {
356
383
  const [isEditMode, setIsEditMode] = useState5(false);
357
384
  useEffect5(() => {
@@ -362,18 +389,29 @@ function useEditMode() {
362
389
  return isEditMode;
363
390
  }
364
391
  function useEditModeMessaging() {
365
- const queryClient = useQueryClient();
392
+ const ctx = useFoxPixelContext();
393
+ const queryClient = ctx?.queryClient ?? null;
366
394
  const isEditMode = useEditMode();
367
395
  useEffect5(() => {
368
396
  if (!isEditMode || typeof window === "undefined") return;
369
397
  window.parent.postMessage({ type: "FOXPIXEL_READY" }, "*");
370
398
  const handleMessage = (event) => {
399
+ if (!queryClient) return;
371
400
  const { type, payload } = event.data || {};
372
- if (type === "FOXPIXEL_CONTENT_UPDATED" && payload?.contentKey) {
373
- queryClient.invalidateQueries({
374
- queryKey: [SITE_CONTENT_QUERY_KEY, payload.contentKey]
375
- });
401
+ if (type !== "FOXPIXEL_CONTENT_UPDATED" || !payload?.contentKey) return;
402
+ const { contentKey, newValue } = payload;
403
+ if (typeof newValue === "string") {
404
+ queryClient.setQueryData(
405
+ [SITE_CONTENT_QUERY_KEY, contentKey],
406
+ (prev) => ({
407
+ value: newValue,
408
+ contentType: prev?.contentType ?? "TEXT"
409
+ })
410
+ );
376
411
  }
412
+ queryClient.invalidateQueries({
413
+ queryKey: [SITE_CONTENT_QUERY_KEY, contentKey]
414
+ });
377
415
  };
378
416
  window.addEventListener("message", handleMessage);
379
417
  return () => window.removeEventListener("message", handleMessage);
@@ -406,16 +444,40 @@ function useSendEditRequest() {
406
444
  }
407
445
 
408
446
  // src/hooks/useSiteContentQuery.ts
409
- import { useQuery } from "@tanstack/react-query";
447
+ import { useState as useState6, useEffect as useEffect6, useRef } from "react";
448
+ function getCached(queryClient, contentKey) {
449
+ const data = queryClient.getQueryData([
450
+ SITE_CONTENT_QUERY_KEY,
451
+ contentKey
452
+ ]);
453
+ if (data == null) return void 0;
454
+ return { value: data.value ?? "", contentType: data.contentType ?? "TEXT" };
455
+ }
410
456
  function useSiteContentQuery(contentKey, options) {
411
457
  const { defaultValue } = options;
412
- const { client } = useFoxPixelContext();
413
- const { data, isLoading } = useQuery({
414
- queryKey: [SITE_CONTENT_QUERY_KEY, contentKey],
415
- queryFn: async () => {
458
+ const { client, queryClient } = useFoxPixelContext();
459
+ const [state, setState] = useState6(() => {
460
+ if (queryClient) {
461
+ const cached = getCached(queryClient, contentKey);
462
+ if (cached) {
463
+ return { value: cached.value, isLoading: false, contentType: cached.contentType };
464
+ }
465
+ }
466
+ return { value: defaultValue, isLoading: true, contentType: "TEXT" };
467
+ });
468
+ const contentKeyRef = useRef(contentKey);
469
+ contentKeyRef.current = contentKey;
470
+ useEffect6(() => {
471
+ if (!queryClient) {
472
+ setState((s) => ({ ...s, value: defaultValue, isLoading: false }));
473
+ return;
474
+ }
475
+ const key = contentKeyRef.current;
476
+ const queryKey = [SITE_CONTENT_QUERY_KEY, key];
477
+ const queryFn = async () => {
416
478
  try {
417
479
  const content = await client.get(
418
- `/api/site/content/${encodeURIComponent(contentKey)}`
480
+ `/api/site/content/${encodeURIComponent(key)}`
419
481
  );
420
482
  if (!content) return null;
421
483
  return {
@@ -423,23 +485,50 @@ function useSiteContentQuery(contentKey, options) {
423
485
  contentType: content.contentType ?? "TEXT"
424
486
  };
425
487
  } catch (err) {
426
- const status = err?.status;
488
+ const status = err?.response?.status;
427
489
  if (status === 404) return null;
428
490
  throw err;
429
491
  }
430
- },
431
- staleTime: 1e3 * 60 * 5,
432
- retry: 1
433
- });
434
- return {
435
- value: data?.value ?? defaultValue,
436
- isLoading,
437
- contentType: data?.contentType ?? "TEXT"
438
- };
492
+ };
493
+ let cancelled = false;
494
+ queryClient.fetchQuery({
495
+ queryKey,
496
+ queryFn,
497
+ staleTime: 1e3 * 60 * 5,
498
+ retry: 1
499
+ }).then((data) => {
500
+ if (cancelled) return;
501
+ setState({
502
+ value: data?.value ?? defaultValue,
503
+ isLoading: false,
504
+ contentType: data?.contentType ?? "TEXT"
505
+ });
506
+ }).catch(() => {
507
+ if (cancelled) return;
508
+ setState((s) => ({ ...s, value: defaultValue, isLoading: false }));
509
+ });
510
+ const unsub = queryClient.getQueryCache().subscribe((event) => {
511
+ if (event?.type === "updated" && event?.query?.queryKey[1] === key) {
512
+ const cached = getCached(queryClient, key);
513
+ if (cached && !cancelled) {
514
+ setState({
515
+ value: cached.value,
516
+ isLoading: false,
517
+ contentType: cached.contentType
518
+ });
519
+ }
520
+ }
521
+ });
522
+ return () => {
523
+ cancelled = true;
524
+ unsub();
525
+ };
526
+ }, [queryClient, contentKey, defaultValue, client]);
527
+ return state;
439
528
  }
440
529
 
441
530
  // src/utils/sanitize.ts
442
- import DOMPurify from "isomorphic-dompurify";
531
+ import sanitizeHtmlLib from "sanitize-html";
443
532
  var DEFAULT_ALLOWED_TAGS = [
444
533
  "p",
445
534
  "br",
@@ -470,13 +559,16 @@ var DEFAULT_ALLOWED_TAGS = [
470
559
  "th",
471
560
  "td"
472
561
  ];
473
- var DEFAULT_ALLOWED_ATTR = ["href", "target", "rel", "src", "alt", "title", "class"];
562
+ var DEFAULT_ALLOWED_ATTR = {
563
+ a: ["href", "target", "rel", "title"],
564
+ img: ["src", "alt", "title", "width", "height"],
565
+ "*": ["class"]
566
+ };
474
567
  function sanitizeHtml(html) {
475
568
  if (typeof html !== "string") return "";
476
- return DOMPurify.sanitize(html, {
477
- ALLOWED_TAGS: DEFAULT_ALLOWED_TAGS,
478
- ALLOWED_ATTR: DEFAULT_ALLOWED_ATTR,
479
- ADD_ATTR: ["target"]
569
+ return sanitizeHtmlLib(html, {
570
+ allowedTags: DEFAULT_ALLOWED_TAGS,
571
+ allowedAttributes: DEFAULT_ALLOWED_ATTR
480
572
  });
481
573
  }
482
574
 
@@ -486,7 +578,24 @@ function cn(...args) {
486
578
  }
487
579
 
488
580
  // src/components/Editable.tsx
489
- import { Fragment as Fragment3, jsx as jsx6, jsxs } from "react/jsx-runtime";
581
+ import { jsx as jsx6, jsxs } from "react/jsx-runtime";
582
+ var EDIT_MODE_TOOLTIP_STYLE = {
583
+ position: "absolute",
584
+ top: "-28px",
585
+ left: "50%",
586
+ transform: "translateX(-50%)",
587
+ fontSize: "10px",
588
+ fontFamily: "system-ui, sans-serif",
589
+ padding: "4px 8px",
590
+ backgroundColor: "rgb(37 99 235)",
591
+ color: "white",
592
+ borderRadius: "4px",
593
+ whiteSpace: "nowrap",
594
+ pointerEvents: "none",
595
+ zIndex: 100,
596
+ boxShadow: "0 1px 3px rgba(0,0,0,0.2)",
597
+ transition: "opacity 0.15s ease"
598
+ };
490
599
  function Editable({
491
600
  contentKey,
492
601
  defaultValue,
@@ -494,12 +603,19 @@ function Editable({
494
603
  multiline = false,
495
604
  className
496
605
  }) {
606
+ const [isHovered, setIsHovered] = useState7(false);
497
607
  const isEditMode = useEditModeMessaging();
498
608
  const sendEditRequest = useSendEditRequest();
499
609
  const { value, isLoading, contentType } = useSiteContentQuery(contentKey, {
500
610
  defaultValue
501
611
  });
502
612
  const section = contentKey.includes(".") ? contentKey.split(".")[0] : void 0;
613
+ useEffect7(() => {
614
+ if (isEditMode) {
615
+ registerContentKey(contentKey);
616
+ return () => unregisterContentKey(contentKey);
617
+ }
618
+ }, [isEditMode, contentKey]);
503
619
  const handleClick = useCallback3(
504
620
  (e) => {
505
621
  if (isEditMode) {
@@ -527,36 +643,59 @@ function Editable({
527
643
  "aria-label": "Loading content..."
528
644
  });
529
645
  }
530
- const editModeStyles = isEditMode ? cn(
531
- "cursor-pointer transition-all duration-200",
532
- "hover:ring-2 hover:ring-blue-500 hover:ring-offset-2",
533
- "hover:bg-blue-50/50 dark:hover:bg-blue-950/30",
534
- "relative group"
535
- ) : "";
646
+ const editModeWrapperStyle = isEditMode ? {
647
+ position: "relative",
648
+ display: "inline",
649
+ cursor: "pointer",
650
+ borderRadius: "2px",
651
+ outline: isHovered ? "2px solid rgb(59 130 246)" : "none",
652
+ outlineOffset: "2px",
653
+ backgroundColor: isHovered ? "rgba(59 130 246 / 0.08)" : void 0
654
+ } : {};
655
+ const tooltipSpan = isEditMode ? /* @__PURE__ */ jsx6(
656
+ "span",
657
+ {
658
+ style: {
659
+ ...EDIT_MODE_TOOLTIP_STYLE,
660
+ opacity: isHovered ? 1 : 0
661
+ },
662
+ "aria-hidden": true,
663
+ children: "Click to edit"
664
+ }
665
+ ) : null;
666
+ const hoverHandlers = isEditMode ? {
667
+ onMouseEnter: () => setIsHovered(true),
668
+ onMouseLeave: () => setIsHovered(false)
669
+ } : {};
536
670
  if (multiline && value.includes("\n")) {
537
671
  const safeBr = sanitizeHtml(value.replace(/\n/g, "<br />"));
538
- return createElement(as, {
539
- className: cn(className, editModeStyles),
540
- "data-content-key": contentKey,
541
- "data-editable": isEditMode ? "true" : void 0,
542
- onClick: isEditMode ? handleClick : void 0,
543
- dangerouslySetInnerHTML: { __html: safeBr },
544
- title: isEditMode ? "Click to edit" : void 0
545
- });
672
+ return createElement(
673
+ "span",
674
+ { style: editModeWrapperStyle, ...hoverHandlers },
675
+ createElement(as, {
676
+ className,
677
+ "data-content-key": contentKey,
678
+ "data-editable": isEditMode ? "true" : void 0,
679
+ onClick: isEditMode ? handleClick : void 0,
680
+ dangerouslySetInnerHTML: { __html: safeBr }
681
+ }),
682
+ tooltipSpan
683
+ );
546
684
  }
547
685
  return createElement(
548
- as,
549
- {
550
- className: cn(className, editModeStyles),
551
- "data-content-key": contentKey,
552
- "data-editable": isEditMode ? "true" : void 0,
553
- onClick: isEditMode ? handleClick : void 0,
554
- title: isEditMode ? "Click to edit" : void 0
555
- },
556
- /* @__PURE__ */ jsxs(Fragment3, { children: [
557
- value,
558
- isEditMode && /* @__PURE__ */ jsx6("span", { className: "absolute -top-6 left-1/2 -translate-x-1/2 px-2 py-0.5 bg-blue-600 text-white text-[10px] rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50", children: "Click to edit" })
559
- ] })
686
+ "span",
687
+ { style: editModeWrapperStyle, ...hoverHandlers },
688
+ createElement(
689
+ as,
690
+ {
691
+ className,
692
+ "data-content-key": contentKey,
693
+ "data-editable": isEditMode ? "true" : void 0,
694
+ onClick: isEditMode ? handleClick : void 0
695
+ },
696
+ value
697
+ ),
698
+ tooltipSpan
560
699
  );
561
700
  }
562
701
  function EditableHTML({
@@ -565,12 +704,19 @@ function EditableHTML({
565
704
  as = "div",
566
705
  className
567
706
  }) {
707
+ const [isHovered, setIsHovered] = useState7(false);
568
708
  const isEditMode = useEditModeMessaging();
569
709
  const sendEditRequest = useSendEditRequest();
570
710
  const { value, isLoading } = useSiteContentQuery(contentKey, {
571
711
  defaultValue
572
712
  });
573
713
  const section = contentKey.includes(".") ? contentKey.split(".")[0] : void 0;
714
+ useEffect7(() => {
715
+ if (isEditMode) {
716
+ registerContentKey(contentKey);
717
+ return () => unregisterContentKey(contentKey);
718
+ }
719
+ }, [isEditMode, contentKey]);
574
720
  const handleClick = useCallback3(
575
721
  (e) => {
576
722
  if (isEditMode) {
@@ -587,21 +733,39 @@ function EditableHTML({
587
733
  "aria-busy": true
588
734
  });
589
735
  }
590
- const editModeStyles = isEditMode ? cn(
591
- "cursor-pointer transition-all duration-200",
592
- "hover:ring-2 hover:ring-blue-500 hover:ring-offset-2",
593
- "hover:bg-blue-50/50 dark:hover:bg-blue-950/30",
594
- "relative group"
595
- ) : "";
736
+ const wrapperStyle = isEditMode ? {
737
+ position: "relative",
738
+ cursor: "pointer",
739
+ borderRadius: "2px",
740
+ outline: isHovered ? "2px solid rgb(59 130 246)" : "none",
741
+ outlineOffset: "2px",
742
+ backgroundColor: isHovered ? "rgba(59 130 246 / 0.08)" : void 0
743
+ } : {};
596
744
  const safeHtml = sanitizeHtml(value);
597
- return createElement(as, {
598
- className: cn("prose prose-slate dark:prose-invert", className, editModeStyles),
599
- "data-content-key": contentKey,
600
- "data-editable": isEditMode ? "true" : void 0,
601
- onClick: isEditMode ? handleClick : void 0,
602
- title: isEditMode ? "Click to edit" : void 0,
603
- dangerouslySetInnerHTML: { __html: safeHtml }
604
- });
745
+ const hoverHandlers = isEditMode ? { onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false) } : {};
746
+ return createElement(
747
+ "div",
748
+ { style: wrapperStyle, ...hoverHandlers },
749
+ createElement(as, {
750
+ className: cn("prose prose-slate dark:prose-invert", className),
751
+ "data-content-key": contentKey,
752
+ "data-editable": isEditMode ? "true" : void 0,
753
+ onClick: isEditMode ? handleClick : void 0,
754
+ dangerouslySetInnerHTML: { __html: safeHtml }
755
+ }),
756
+ isEditMode && /* @__PURE__ */ jsx6(
757
+ "span",
758
+ {
759
+ style: {
760
+ ...EDIT_MODE_TOOLTIP_STYLE,
761
+ top: "-24px",
762
+ opacity: isHovered ? 1 : 0
763
+ },
764
+ "aria-hidden": true,
765
+ children: "Click to edit"
766
+ }
767
+ )
768
+ );
605
769
  }
606
770
  function EditableImage({
607
771
  contentKey,
@@ -612,12 +776,19 @@ function EditableImage({
612
776
  height,
613
777
  priority = false
614
778
  }) {
779
+ const [isHovered, setIsHovered] = useState7(false);
615
780
  const isEditMode = useEditModeMessaging();
616
781
  const sendEditRequest = useSendEditRequest();
617
782
  const { value: src, isLoading } = useSiteContentQuery(contentKey, {
618
783
  defaultValue
619
784
  });
620
785
  const section = contentKey.includes(".") ? contentKey.split(".")[0] : void 0;
786
+ useEffect7(() => {
787
+ if (isEditMode) {
788
+ registerContentKey(contentKey);
789
+ return () => unregisterContentKey(contentKey);
790
+ }
791
+ }, [isEditMode, contentKey]);
621
792
  const handleClick = useCallback3(
622
793
  (e) => {
623
794
  if (isEditMode) {
@@ -638,39 +809,55 @@ function EditableImage({
638
809
  }
639
810
  );
640
811
  }
641
- const editModeStyles = isEditMode ? cn(
642
- "cursor-pointer transition-all duration-200",
643
- "hover:ring-2 hover:ring-blue-500 hover:ring-offset-2",
644
- "hover:opacity-90",
645
- "relative group"
646
- ) : "";
647
- return /* @__PURE__ */ jsxs("div", { className: cn("relative", isEditMode && "group"), children: [
812
+ const wrapperStyle = isEditMode ? {
813
+ position: "relative",
814
+ display: "inline-block",
815
+ cursor: "pointer",
816
+ borderRadius: "2px",
817
+ outline: isHovered ? "2px solid rgb(59 130 246)" : "none",
818
+ outlineOffset: "2px"
819
+ } : {};
820
+ const hoverHandlers = isEditMode ? { onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false) } : {};
821
+ return /* @__PURE__ */ jsxs("div", { style: wrapperStyle, ...hoverHandlers, children: [
648
822
  /* @__PURE__ */ jsx6(
649
823
  "img",
650
824
  {
651
825
  src,
652
826
  alt,
653
- className: cn(className, editModeStyles),
827
+ className,
828
+ style: isEditMode && isHovered ? { opacity: 0.95 } : void 0,
654
829
  width,
655
830
  height,
656
831
  loading: priority ? "eager" : "lazy",
657
832
  "data-content-key": contentKey,
658
833
  "data-editable": isEditMode ? "true" : void 0,
659
- onClick: isEditMode ? handleClick : void 0,
660
- title: isEditMode ? "Click to edit image" : void 0
834
+ onClick: isEditMode ? handleClick : void 0
661
835
  }
662
836
  ),
663
- isEditMode && /* @__PURE__ */ jsx6("span", { className: "absolute top-2 left-2 px-2 py-0.5 bg-blue-600 text-white text-[10px] rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none", children: "Click to edit image" })
837
+ isEditMode && /* @__PURE__ */ jsx6(
838
+ "span",
839
+ {
840
+ style: {
841
+ ...EDIT_MODE_TOOLTIP_STYLE,
842
+ top: "8px",
843
+ left: "8px",
844
+ transform: "none",
845
+ opacity: isHovered ? 1 : 0
846
+ },
847
+ "aria-hidden": true,
848
+ children: "Click to edit image"
849
+ }
850
+ )
664
851
  ] });
665
852
  }
666
853
 
667
854
  // src/hooks/useServices.ts
668
- import { useState as useState6, useEffect as useEffect6 } from "react";
855
+ import { useState as useState8, useEffect as useEffect8 } from "react";
669
856
  function useServices(options = {}) {
670
857
  const { client } = useFoxPixelContext();
671
- const [services, setServices] = useState6(null);
672
- const [isLoading, setIsLoading] = useState6(true);
673
- const [error, setError] = useState6(null);
858
+ const [services, setServices] = useState8(null);
859
+ const [isLoading, setIsLoading] = useState8(true);
860
+ const [error, setError] = useState8(null);
674
861
  const fetchServices = async () => {
675
862
  try {
676
863
  setIsLoading(true);
@@ -688,7 +875,7 @@ function useServices(options = {}) {
688
875
  setIsLoading(false);
689
876
  }
690
877
  };
691
- useEffect6(() => {
878
+ useEffect8(() => {
692
879
  fetchServices();
693
880
  }, [options.category, options.active]);
694
881
  return {
@@ -700,11 +887,11 @@ function useServices(options = {}) {
700
887
  }
701
888
 
702
889
  // src/hooks/useLeadCapture.ts
703
- import { useState as useState7 } from "react";
890
+ import { useState as useState9 } from "react";
704
891
  function useLeadCapture() {
705
892
  const { client } = useFoxPixelContext();
706
- const [isLoading, setIsLoading] = useState7(false);
707
- const [error, setError] = useState7(null);
893
+ const [isLoading, setIsLoading] = useState9(false);
894
+ const [error, setError] = useState9(null);
708
895
  const captureLead = async (data) => {
709
896
  try {
710
897
  setIsLoading(true);
@@ -727,11 +914,11 @@ function useLeadCapture() {
727
914
  }
728
915
 
729
916
  // src/hooks/useContactCapture.ts
730
- import { useState as useState8 } from "react";
917
+ import { useState as useState10 } from "react";
731
918
  function useContactCapture() {
732
919
  const { client } = useFoxPixelContext();
733
- const [isLoading, setIsLoading] = useState8(false);
734
- const [error, setError] = useState8(null);
920
+ const [isLoading, setIsLoading] = useState10(false);
921
+ const [error, setError] = useState10(null);
735
922
  const captureContact = async (data) => {
736
923
  try {
737
924
  setIsLoading(true);
@@ -754,14 +941,14 @@ function useContactCapture() {
754
941
  }
755
942
 
756
943
  // src/hooks/useSiteContent.ts
757
- import { useState as useState9, useEffect as useEffect7, useCallback as useCallback4 } from "react";
944
+ import { useState as useState11, useEffect as useEffect9, useCallback as useCallback4 } from "react";
758
945
  function useSiteContent(contentKey, options = {}) {
759
946
  const { defaultValue = "", fetchOnMount = true } = options;
760
947
  const { client } = useFoxPixelContext();
761
948
  const { user, hasPermission } = useAuth();
762
- const [data, setData] = useState9(null);
763
- const [isLoading, setIsLoading] = useState9(fetchOnMount);
764
- const [error, setError] = useState9(null);
949
+ const [data, setData] = useState11(null);
950
+ const [isLoading, setIsLoading] = useState11(fetchOnMount);
951
+ const [error, setError] = useState11(null);
765
952
  const canEdit = user !== null && hasPermission("site:content:update");
766
953
  const fetchContent = useCallback4(async () => {
767
954
  try {
@@ -794,7 +981,7 @@ function useSiteContent(contentKey, options = {}) {
794
981
  throw err;
795
982
  }
796
983
  }, [client, contentKey]);
797
- useEffect7(() => {
984
+ useEffect9(() => {
798
985
  if (fetchOnMount) {
799
986
  fetchContent();
800
987
  }
@@ -813,9 +1000,9 @@ function useSiteContent(contentKey, options = {}) {
813
1000
  function useSiteContents(contentKeys, options = {}) {
814
1001
  const { defaults = {} } = options;
815
1002
  const { client } = useFoxPixelContext();
816
- const [data, setData] = useState9({});
817
- const [isLoading, setIsLoading] = useState9(true);
818
- const [error, setError] = useState9(null);
1003
+ const [data, setData] = useState11({});
1004
+ const [isLoading, setIsLoading] = useState11(true);
1005
+ const [error, setError] = useState11(null);
819
1006
  const fetchContents = useCallback4(async () => {
820
1007
  if (contentKeys.length === 0) {
821
1008
  setData({});
@@ -836,7 +1023,7 @@ function useSiteContents(contentKeys, options = {}) {
836
1023
  setIsLoading(false);
837
1024
  }
838
1025
  }, [client, contentKeys.join(",")]);
839
- useEffect7(() => {
1026
+ useEffect9(() => {
840
1027
  fetchContents();
841
1028
  }, [fetchContents]);
842
1029
  const getValue = useCallback4((key, defaultValue) => {
@@ -856,9 +1043,9 @@ function useSiteContents(contentKeys, options = {}) {
856
1043
  }
857
1044
  function useSiteContentSection(section) {
858
1045
  const { client } = useFoxPixelContext();
859
- const [contents, setContents] = useState9([]);
860
- const [isLoading, setIsLoading] = useState9(true);
861
- const [error, setError] = useState9(null);
1046
+ const [contents, setContents] = useState11([]);
1047
+ const [isLoading, setIsLoading] = useState11(true);
1048
+ const [error, setError] = useState11(null);
862
1049
  const fetchContents = useCallback4(async () => {
863
1050
  try {
864
1051
  setIsLoading(true);
@@ -873,7 +1060,7 @@ function useSiteContentSection(section) {
873
1060
  setIsLoading(false);
874
1061
  }
875
1062
  }, [client, section]);
876
- useEffect7(() => {
1063
+ useEffect9(() => {
877
1064
  fetchContents();
878
1065
  }, [fetchContents]);
879
1066
  return {
@@ -884,13 +1071,43 @@ function useSiteContentSection(section) {
884
1071
  };
885
1072
  }
886
1073
 
1074
+ // src/prefetchSiteContent.ts
1075
+ async function prefetchSiteContent(queryClient, options) {
1076
+ const { apiUrl, apiKey, tenantId, contentKeys } = options;
1077
+ const client = new FoxPixelHttpClient({ apiUrl, apiKey, tenantId });
1078
+ await Promise.all(
1079
+ contentKeys.map(async (contentKey) => {
1080
+ try {
1081
+ const content = await client.get(
1082
+ `/api/site/content/${encodeURIComponent(contentKey)}`
1083
+ );
1084
+ queryClient.setQueryData(
1085
+ [SITE_CONTENT_QUERY_KEY, contentKey],
1086
+ {
1087
+ value: content?.value ?? "",
1088
+ contentType: content?.contentType ?? "TEXT"
1089
+ }
1090
+ );
1091
+ } catch (err) {
1092
+ const status = err?.response?.status;
1093
+ if (status === 404) {
1094
+ queryClient.setQueryData([SITE_CONTENT_QUERY_KEY, contentKey], {
1095
+ value: "",
1096
+ contentType: "TEXT"
1097
+ });
1098
+ }
1099
+ }
1100
+ })
1101
+ );
1102
+ }
1103
+
887
1104
  // src/blog/hooks.ts
888
- import { useState as useState10, useEffect as useEffect8 } from "react";
1105
+ import { useState as useState12, useEffect as useEffect10 } from "react";
889
1106
  function useBlogPosts(options = {}) {
890
1107
  const { client } = useFoxPixelContext();
891
- const [data, setData] = useState10(null);
892
- const [isLoading, setIsLoading] = useState10(true);
893
- const [error, setError] = useState10(null);
1108
+ const [data, setData] = useState12(null);
1109
+ const [isLoading, setIsLoading] = useState12(true);
1110
+ const [error, setError] = useState12(null);
894
1111
  const page = options.page ?? 0;
895
1112
  const limit = options.limit ?? 10;
896
1113
  const fetchPosts = async () => {
@@ -910,7 +1127,7 @@ function useBlogPosts(options = {}) {
910
1127
  setIsLoading(false);
911
1128
  }
912
1129
  };
913
- useEffect8(() => {
1130
+ useEffect10(() => {
914
1131
  fetchPosts();
915
1132
  }, [page, limit]);
916
1133
  return {
@@ -922,9 +1139,9 @@ function useBlogPosts(options = {}) {
922
1139
  }
923
1140
  function useBlogPost(slug) {
924
1141
  const { client } = useFoxPixelContext();
925
- const [data, setData] = useState10(null);
926
- const [isLoading, setIsLoading] = useState10(!!slug);
927
- const [error, setError] = useState10(null);
1142
+ const [data, setData] = useState12(null);
1143
+ const [isLoading, setIsLoading] = useState12(!!slug);
1144
+ const [error, setError] = useState12(null);
928
1145
  const fetchPost = async () => {
929
1146
  if (!slug) {
930
1147
  setData(null);
@@ -943,7 +1160,7 @@ function useBlogPost(slug) {
943
1160
  setIsLoading(false);
944
1161
  }
945
1162
  };
946
- useEffect8(() => {
1163
+ useEffect10(() => {
947
1164
  fetchPost();
948
1165
  }, [slug]);
949
1166
  return {
@@ -955,9 +1172,9 @@ function useBlogPost(slug) {
955
1172
  }
956
1173
  function useBlogCategories() {
957
1174
  const { client } = useFoxPixelContext();
958
- const [data, setData] = useState10(null);
959
- const [isLoading, setIsLoading] = useState10(true);
960
- const [error, setError] = useState10(null);
1175
+ const [data, setData] = useState12(null);
1176
+ const [isLoading, setIsLoading] = useState12(true);
1177
+ const [error, setError] = useState12(null);
961
1178
  const fetchCategories = async () => {
962
1179
  try {
963
1180
  setIsLoading(true);
@@ -971,7 +1188,7 @@ function useBlogCategories() {
971
1188
  setIsLoading(false);
972
1189
  }
973
1190
  };
974
- useEffect8(() => {
1191
+ useEffect10(() => {
975
1192
  fetchCategories();
976
1193
  }, []);
977
1194
  return {
@@ -983,9 +1200,9 @@ function useBlogCategories() {
983
1200
  }
984
1201
  function useBlogTags() {
985
1202
  const { client } = useFoxPixelContext();
986
- const [data, setData] = useState10(null);
987
- const [isLoading, setIsLoading] = useState10(true);
988
- const [error, setError] = useState10(null);
1203
+ const [data, setData] = useState12(null);
1204
+ const [isLoading, setIsLoading] = useState12(true);
1205
+ const [error, setError] = useState12(null);
989
1206
  const fetchTags = async () => {
990
1207
  try {
991
1208
  setIsLoading(true);
@@ -999,7 +1216,7 @@ function useBlogTags() {
999
1216
  setIsLoading(false);
1000
1217
  }
1001
1218
  };
1002
- useEffect8(() => {
1219
+ useEffect10(() => {
1003
1220
  fetchTags();
1004
1221
  }, []);
1005
1222
  return {
@@ -1011,9 +1228,9 @@ function useBlogTags() {
1011
1228
  }
1012
1229
  function useBlogComments(slug) {
1013
1230
  const { client } = useFoxPixelContext();
1014
- const [data, setData] = useState10(null);
1015
- const [isLoading, setIsLoading] = useState10(!!slug);
1016
- const [error, setError] = useState10(null);
1231
+ const [data, setData] = useState12(null);
1232
+ const [isLoading, setIsLoading] = useState12(!!slug);
1233
+ const [error, setError] = useState12(null);
1017
1234
  const fetchComments = async () => {
1018
1235
  if (!slug) {
1019
1236
  setData(null);
@@ -1034,7 +1251,7 @@ function useBlogComments(slug) {
1034
1251
  setIsLoading(false);
1035
1252
  }
1036
1253
  };
1037
- useEffect8(() => {
1254
+ useEffect10(() => {
1038
1255
  fetchComments();
1039
1256
  }, [slug]);
1040
1257
  return {
@@ -1046,8 +1263,8 @@ function useBlogComments(slug) {
1046
1263
  }
1047
1264
  function useBlogCommentSubmit(slug) {
1048
1265
  const { client } = useFoxPixelContext();
1049
- const [isSubmitting, setIsSubmitting] = useState10(false);
1050
- const [error, setError] = useState10(null);
1266
+ const [isSubmitting, setIsSubmitting] = useState12(false);
1267
+ const [error, setError] = useState12(null);
1051
1268
  const submit = async (payload) => {
1052
1269
  if (!slug) return null;
1053
1270
  try {
@@ -1075,9 +1292,9 @@ function useBlogCommentSubmit(slug) {
1075
1292
  }
1076
1293
  function useBlogFeaturedPosts(limit = 6) {
1077
1294
  const { client } = useFoxPixelContext();
1078
- const [data, setData] = useState10(null);
1079
- const [isLoading, setIsLoading] = useState10(true);
1080
- const [error, setError] = useState10(null);
1295
+ const [data, setData] = useState12(null);
1296
+ const [isLoading, setIsLoading] = useState12(true);
1297
+ const [error, setError] = useState12(null);
1081
1298
  const fetchFeatured = async () => {
1082
1299
  try {
1083
1300
  setIsLoading(true);
@@ -1095,7 +1312,7 @@ function useBlogFeaturedPosts(limit = 6) {
1095
1312
  setIsLoading(false);
1096
1313
  }
1097
1314
  };
1098
- useEffect8(() => {
1315
+ useEffect10(() => {
1099
1316
  fetchFeatured();
1100
1317
  }, [limit]);
1101
1318
  return {
@@ -1107,9 +1324,9 @@ function useBlogFeaturedPosts(limit = 6) {
1107
1324
  }
1108
1325
  function useNewsletterSubscribe() {
1109
1326
  const { client } = useFoxPixelContext();
1110
- const [isSubmitting, setIsSubmitting] = useState10(false);
1111
- const [error, setError] = useState10(null);
1112
- const [success, setSuccess] = useState10(false);
1327
+ const [isSubmitting, setIsSubmitting] = useState12(false);
1328
+ const [error, setError] = useState12(null);
1329
+ const [success, setSuccess] = useState12(false);
1113
1330
  const subscribe = async (payload) => {
1114
1331
  try {
1115
1332
  setIsSubmitting(true);
@@ -1142,9 +1359,9 @@ function useNewsletterSubscribe() {
1142
1359
  }
1143
1360
  function useNewsletterUnsubscribe() {
1144
1361
  const { client } = useFoxPixelContext();
1145
- const [isSubmitting, setIsSubmitting] = useState10(false);
1146
- const [error, setError] = useState10(null);
1147
- const [success, setSuccess] = useState10(false);
1362
+ const [isSubmitting, setIsSubmitting] = useState12(false);
1363
+ const [error, setError] = useState12(null);
1364
+ const [success, setSuccess] = useState12(false);
1148
1365
  const unsubscribe = async (email) => {
1149
1366
  try {
1150
1367
  setIsSubmitting(true);
@@ -1189,12 +1406,12 @@ function useNewsletterUnsubscribe() {
1189
1406
  }
1190
1407
 
1191
1408
  // src/blog/admin-hooks.ts
1192
- import { useState as useState11, useEffect as useEffect9, useCallback as useCallback5 } from "react";
1409
+ import { useState as useState13, useEffect as useEffect11, useCallback as useCallback5 } from "react";
1193
1410
  function useAdminBlogPosts(options = {}) {
1194
1411
  const { client } = useFoxPixelContext();
1195
- const [data, setData] = useState11(null);
1196
- const [isLoading, setIsLoading] = useState11(true);
1197
- const [error, setError] = useState11(null);
1412
+ const [data, setData] = useState13(null);
1413
+ const [isLoading, setIsLoading] = useState13(true);
1414
+ const [error, setError] = useState13(null);
1198
1415
  const page = options.page ?? 0;
1199
1416
  const size = options.size ?? 20;
1200
1417
  const fetchPosts = useCallback5(async () => {
@@ -1212,16 +1429,16 @@ function useAdminBlogPosts(options = {}) {
1212
1429
  setIsLoading(false);
1213
1430
  }
1214
1431
  }, [client, page, size]);
1215
- useEffect9(() => {
1432
+ useEffect11(() => {
1216
1433
  fetchPosts();
1217
1434
  }, [fetchPosts]);
1218
1435
  return { data, isLoading, error, refetch: fetchPosts };
1219
1436
  }
1220
1437
  function useAdminBlogPost(id) {
1221
1438
  const { client } = useFoxPixelContext();
1222
- const [data, setData] = useState11(null);
1223
- const [isLoading, setIsLoading] = useState11(!!id);
1224
- const [error, setError] = useState11(null);
1439
+ const [data, setData] = useState13(null);
1440
+ const [isLoading, setIsLoading] = useState13(!!id);
1441
+ const [error, setError] = useState13(null);
1225
1442
  const fetchPost = useCallback5(async () => {
1226
1443
  if (!id) {
1227
1444
  setData(null);
@@ -1239,15 +1456,15 @@ function useAdminBlogPost(id) {
1239
1456
  setIsLoading(false);
1240
1457
  }
1241
1458
  }, [client, id]);
1242
- useEffect9(() => {
1459
+ useEffect11(() => {
1243
1460
  fetchPost();
1244
1461
  }, [fetchPost]);
1245
1462
  return { data, isLoading, error, refetch: fetchPost };
1246
1463
  }
1247
1464
  function useAdminBlogPostMutations() {
1248
1465
  const { client } = useFoxPixelContext();
1249
- const [isLoading, setIsLoading] = useState11(false);
1250
- const [error, setError] = useState11(null);
1466
+ const [isLoading, setIsLoading] = useState13(false);
1467
+ const [error, setError] = useState13(null);
1251
1468
  const create = async (payload) => {
1252
1469
  try {
1253
1470
  setIsLoading(true);
@@ -1291,9 +1508,9 @@ function useAdminBlogPostMutations() {
1291
1508
  }
1292
1509
  function useAdminBlogCategories() {
1293
1510
  const { client } = useFoxPixelContext();
1294
- const [data, setData] = useState11(null);
1295
- const [isLoading, setIsLoading] = useState11(true);
1296
- const [error, setError] = useState11(null);
1511
+ const [data, setData] = useState13(null);
1512
+ const [isLoading, setIsLoading] = useState13(true);
1513
+ const [error, setError] = useState13(null);
1297
1514
  const fetchCategories = useCallback5(async () => {
1298
1515
  try {
1299
1516
  setIsLoading(true);
@@ -1306,7 +1523,7 @@ function useAdminBlogCategories() {
1306
1523
  setIsLoading(false);
1307
1524
  }
1308
1525
  }, [client]);
1309
- useEffect9(() => {
1526
+ useEffect11(() => {
1310
1527
  fetchCategories();
1311
1528
  }, [fetchCategories]);
1312
1529
  const create = async (payload) => {
@@ -1343,9 +1560,9 @@ function useAdminBlogCategories() {
1343
1560
  }
1344
1561
  function useAdminBlogTags() {
1345
1562
  const { client } = useFoxPixelContext();
1346
- const [data, setData] = useState11(null);
1347
- const [isLoading, setIsLoading] = useState11(true);
1348
- const [error, setError] = useState11(null);
1563
+ const [data, setData] = useState13(null);
1564
+ const [isLoading, setIsLoading] = useState13(true);
1565
+ const [error, setError] = useState13(null);
1349
1566
  const fetchTags = useCallback5(async () => {
1350
1567
  try {
1351
1568
  setIsLoading(true);
@@ -1358,7 +1575,7 @@ function useAdminBlogTags() {
1358
1575
  setIsLoading(false);
1359
1576
  }
1360
1577
  }, [client]);
1361
- useEffect9(() => {
1578
+ useEffect11(() => {
1362
1579
  fetchTags();
1363
1580
  }, [fetchTags]);
1364
1581
  const create = async (payload) => {
@@ -1395,9 +1612,9 @@ function useAdminBlogTags() {
1395
1612
  }
1396
1613
  function useAdminBlogComments(options = {}) {
1397
1614
  const { client } = useFoxPixelContext();
1398
- const [data, setData] = useState11(null);
1399
- const [isLoading, setIsLoading] = useState11(true);
1400
- const [error, setError] = useState11(null);
1615
+ const [data, setData] = useState13(null);
1616
+ const [isLoading, setIsLoading] = useState13(true);
1617
+ const [error, setError] = useState13(null);
1401
1618
  const { status, postId, page = 0, size = 20 } = options;
1402
1619
  const fetchComments = useCallback5(async () => {
1403
1620
  try {
@@ -1416,7 +1633,7 @@ function useAdminBlogComments(options = {}) {
1416
1633
  setIsLoading(false);
1417
1634
  }
1418
1635
  }, [client, status, postId, page, size]);
1419
- useEffect9(() => {
1636
+ useEffect11(() => {
1420
1637
  fetchComments();
1421
1638
  }, [fetchComments]);
1422
1639
  const updateStatus = async (id, newStatus) => {
@@ -1443,9 +1660,9 @@ function useAdminBlogComments(options = {}) {
1443
1660
  }
1444
1661
  function useAdminNewsletterSubscribers(options = {}) {
1445
1662
  const { client } = useFoxPixelContext();
1446
- const [data, setData] = useState11(null);
1447
- const [isLoading, setIsLoading] = useState11(true);
1448
- const [error, setError] = useState11(null);
1663
+ const [data, setData] = useState13(null);
1664
+ const [isLoading, setIsLoading] = useState13(true);
1665
+ const [error, setError] = useState13(null);
1449
1666
  const { status, page = 0, size = 20 } = options;
1450
1667
  const fetchSubscribers = useCallback5(async () => {
1451
1668
  try {
@@ -1463,7 +1680,7 @@ function useAdminNewsletterSubscribers(options = {}) {
1463
1680
  setIsLoading(false);
1464
1681
  }
1465
1682
  }, [client, status, page, size]);
1466
- useEffect9(() => {
1683
+ useEffect11(() => {
1467
1684
  fetchSubscribers();
1468
1685
  }, [fetchSubscribers]);
1469
1686
  const remove = async (id) => {
@@ -1480,9 +1697,9 @@ function useAdminNewsletterSubscribers(options = {}) {
1480
1697
  }
1481
1698
  function useAdminNewsletterStats() {
1482
1699
  const { client } = useFoxPixelContext();
1483
- const [data, setData] = useState11(null);
1484
- const [isLoading, setIsLoading] = useState11(true);
1485
- const [error, setError] = useState11(null);
1700
+ const [data, setData] = useState13(null);
1701
+ const [isLoading, setIsLoading] = useState13(true);
1702
+ const [error, setError] = useState13(null);
1486
1703
  const fetchStats = useCallback5(async () => {
1487
1704
  try {
1488
1705
  setIsLoading(true);
@@ -1495,16 +1712,16 @@ function useAdminNewsletterStats() {
1495
1712
  setIsLoading(false);
1496
1713
  }
1497
1714
  }, [client]);
1498
- useEffect9(() => {
1715
+ useEffect11(() => {
1499
1716
  fetchStats();
1500
1717
  }, [fetchStats]);
1501
1718
  return { data, isLoading, error, refetch: fetchStats };
1502
1719
  }
1503
1720
  function useAdminBlogSettings() {
1504
1721
  const { client } = useFoxPixelContext();
1505
- const [data, setData] = useState11(null);
1506
- const [isLoading, setIsLoading] = useState11(true);
1507
- const [error, setError] = useState11(null);
1722
+ const [data, setData] = useState13(null);
1723
+ const [isLoading, setIsLoading] = useState13(true);
1724
+ const [error, setError] = useState13(null);
1508
1725
  const fetchSettings = useCallback5(async () => {
1509
1726
  try {
1510
1727
  setIsLoading(true);
@@ -1517,7 +1734,7 @@ function useAdminBlogSettings() {
1517
1734
  setIsLoading(false);
1518
1735
  }
1519
1736
  }, [client]);
1520
- useEffect9(() => {
1737
+ useEffect11(() => {
1521
1738
  fetchSettings();
1522
1739
  }, [fetchSettings]);
1523
1740
  const update = async (settings) => {
@@ -1534,9 +1751,9 @@ function useAdminBlogSettings() {
1534
1751
  }
1535
1752
  function useAdminBlogAnalytics() {
1536
1753
  const { client } = useFoxPixelContext();
1537
- const [data, setData] = useState11(null);
1538
- const [isLoading, setIsLoading] = useState11(true);
1539
- const [error, setError] = useState11(null);
1754
+ const [data, setData] = useState13(null);
1755
+ const [isLoading, setIsLoading] = useState13(true);
1756
+ const [error, setError] = useState13(null);
1540
1757
  const fetchAnalytics = useCallback5(async () => {
1541
1758
  try {
1542
1759
  setIsLoading(true);
@@ -1549,7 +1766,7 @@ function useAdminBlogAnalytics() {
1549
1766
  setIsLoading(false);
1550
1767
  }
1551
1768
  }, [client]);
1552
- useEffect9(() => {
1769
+ useEffect11(() => {
1553
1770
  fetchAnalytics();
1554
1771
  }, [fetchAnalytics]);
1555
1772
  return { data, isLoading, error, refetch: fetchAnalytics };
@@ -1597,6 +1814,7 @@ export {
1597
1814
  ProtectedRoute,
1598
1815
  SITE_CONTENT_QUERY_KEY,
1599
1816
  getBlogPostSchemaLd,
1817
+ prefetchSiteContent,
1600
1818
  useAdminBlogAnalytics,
1601
1819
  useAdminBlogCategories,
1602
1820
  useAdminBlogComments,