@foxpixel/react 0.1.1 → 0.2.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/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() {
@@ -238,7 +244,8 @@ function AuthProvider({
238
244
  logout,
239
245
  register,
240
246
  updateProfile,
241
- refetch: fetchCurrentUser
247
+ refetch: fetchCurrentUser,
248
+ hasPermission: (permission) => user !== null && permission === "site:content:update"
242
249
  };
243
250
  return /* @__PURE__ */ jsx2(AuthContext.Provider, { value, children });
244
251
  }
@@ -344,13 +351,473 @@ function withAuth(Component, options = {}) {
344
351
  };
345
352
  }
346
353
 
354
+ // src/components/Editable.tsx
355
+ import { createElement, useCallback as useCallback3, useState as useState7 } from "react";
356
+
357
+ // src/hooks/useEditMode.ts
358
+ import { useEffect as useEffect5, useState as useState5, useCallback as useCallback2 } from "react";
359
+ var SITE_CONTENT_QUERY_KEY = "siteContent";
360
+ function useEditMode() {
361
+ const [isEditMode, setIsEditMode] = useState5(false);
362
+ useEffect5(() => {
363
+ if (typeof window === "undefined") return;
364
+ const params = new URLSearchParams(window.location.search);
365
+ setIsEditMode(params.get("edit-mode") === "true");
366
+ }, []);
367
+ return isEditMode;
368
+ }
369
+ function useEditModeMessaging() {
370
+ const ctx = useFoxPixelContext();
371
+ const queryClient = ctx?.queryClient ?? null;
372
+ const isEditMode = useEditMode();
373
+ useEffect5(() => {
374
+ if (!isEditMode || typeof window === "undefined") return;
375
+ window.parent.postMessage({ type: "FOXPIXEL_READY" }, "*");
376
+ const handleMessage = (event) => {
377
+ if (!queryClient) return;
378
+ const { type, payload } = event.data || {};
379
+ if (type !== "FOXPIXEL_CONTENT_UPDATED" || !payload?.contentKey) return;
380
+ const { contentKey, newValue } = payload;
381
+ if (typeof newValue === "string") {
382
+ queryClient.setQueryData(
383
+ [SITE_CONTENT_QUERY_KEY, contentKey],
384
+ (prev) => ({
385
+ value: newValue,
386
+ contentType: prev?.contentType ?? "TEXT"
387
+ })
388
+ );
389
+ }
390
+ queryClient.invalidateQueries({
391
+ queryKey: [SITE_CONTENT_QUERY_KEY, contentKey]
392
+ });
393
+ };
394
+ window.addEventListener("message", handleMessage);
395
+ return () => window.removeEventListener("message", handleMessage);
396
+ }, [isEditMode, queryClient]);
397
+ return isEditMode;
398
+ }
399
+ function useSendEditRequest() {
400
+ const isEditMode = useEditMode();
401
+ return useCallback2(
402
+ (contentKey, currentValue, contentType = "text", section, description) => {
403
+ if (!isEditMode) return;
404
+ if (typeof window !== "undefined" && window.parent !== window) {
405
+ window.parent.postMessage(
406
+ {
407
+ type: "FOXPIXEL_EDIT_CONTENT",
408
+ payload: {
409
+ contentKey,
410
+ currentValue,
411
+ contentType,
412
+ section,
413
+ description
414
+ }
415
+ },
416
+ "*"
417
+ );
418
+ }
419
+ },
420
+ [isEditMode]
421
+ );
422
+ }
423
+
424
+ // src/hooks/useSiteContentQuery.ts
425
+ import { useState as useState6, useEffect as useEffect6, useRef } from "react";
426
+ function getCached(queryClient, contentKey) {
427
+ const data = queryClient.getQueryData([
428
+ SITE_CONTENT_QUERY_KEY,
429
+ contentKey
430
+ ]);
431
+ if (data == null) return void 0;
432
+ return { value: data.value ?? "", contentType: data.contentType ?? "TEXT" };
433
+ }
434
+ function useSiteContentQuery(contentKey, options) {
435
+ const { defaultValue } = options;
436
+ const { client, queryClient } = useFoxPixelContext();
437
+ const [state, setState] = useState6(() => {
438
+ if (queryClient) {
439
+ const cached = getCached(queryClient, contentKey);
440
+ if (cached) {
441
+ return { value: cached.value, isLoading: false, contentType: cached.contentType };
442
+ }
443
+ }
444
+ return { value: defaultValue, isLoading: true, contentType: "TEXT" };
445
+ });
446
+ const contentKeyRef = useRef(contentKey);
447
+ contentKeyRef.current = contentKey;
448
+ useEffect6(() => {
449
+ if (!queryClient) {
450
+ setState((s) => ({ ...s, value: defaultValue, isLoading: false }));
451
+ return;
452
+ }
453
+ const key = contentKeyRef.current;
454
+ const queryKey = [SITE_CONTENT_QUERY_KEY, key];
455
+ const queryFn = async () => {
456
+ try {
457
+ const content = await client.get(
458
+ `/api/site/content/${encodeURIComponent(key)}`
459
+ );
460
+ if (!content) return null;
461
+ return {
462
+ value: content.value ?? "",
463
+ contentType: content.contentType ?? "TEXT"
464
+ };
465
+ } catch (err) {
466
+ const status = err?.response?.status;
467
+ if (status === 404) return null;
468
+ throw err;
469
+ }
470
+ };
471
+ let cancelled = false;
472
+ queryClient.fetchQuery({
473
+ queryKey,
474
+ queryFn,
475
+ staleTime: 1e3 * 60 * 5,
476
+ retry: 1
477
+ }).then((data) => {
478
+ if (cancelled) return;
479
+ setState({
480
+ value: data?.value ?? defaultValue,
481
+ isLoading: false,
482
+ contentType: data?.contentType ?? "TEXT"
483
+ });
484
+ }).catch(() => {
485
+ if (cancelled) return;
486
+ setState((s) => ({ ...s, value: defaultValue, isLoading: false }));
487
+ });
488
+ const unsub = queryClient.getQueryCache().subscribe((event) => {
489
+ if (event?.type === "updated" && event?.query?.queryKey[1] === key) {
490
+ const cached = getCached(queryClient, key);
491
+ if (cached && !cancelled) {
492
+ setState({
493
+ value: cached.value,
494
+ isLoading: false,
495
+ contentType: cached.contentType
496
+ });
497
+ }
498
+ }
499
+ });
500
+ return () => {
501
+ cancelled = true;
502
+ unsub();
503
+ };
504
+ }, [queryClient, contentKey, defaultValue, client]);
505
+ return state;
506
+ }
507
+
508
+ // src/utils/sanitize.ts
509
+ import sanitizeHtmlLib from "sanitize-html";
510
+ var DEFAULT_ALLOWED_TAGS = [
511
+ "p",
512
+ "br",
513
+ "strong",
514
+ "em",
515
+ "u",
516
+ "s",
517
+ "a",
518
+ "ul",
519
+ "ol",
520
+ "li",
521
+ "h1",
522
+ "h2",
523
+ "h3",
524
+ "h4",
525
+ "h5",
526
+ "h6",
527
+ "blockquote",
528
+ "code",
529
+ "pre",
530
+ "span",
531
+ "div",
532
+ "img",
533
+ "table",
534
+ "thead",
535
+ "tbody",
536
+ "tr",
537
+ "th",
538
+ "td"
539
+ ];
540
+ var DEFAULT_ALLOWED_ATTR = {
541
+ a: ["href", "target", "rel", "title"],
542
+ img: ["src", "alt", "title", "width", "height"],
543
+ "*": ["class"]
544
+ };
545
+ function sanitizeHtml(html) {
546
+ if (typeof html !== "string") return "";
547
+ return sanitizeHtmlLib(html, {
548
+ allowedTags: DEFAULT_ALLOWED_TAGS,
549
+ allowedAttributes: DEFAULT_ALLOWED_ATTR
550
+ });
551
+ }
552
+
553
+ // src/utils/cn.ts
554
+ function cn(...args) {
555
+ return args.filter(Boolean).join(" ");
556
+ }
557
+
558
+ // src/components/Editable.tsx
559
+ import { jsx as jsx6, jsxs } from "react/jsx-runtime";
560
+ var EDIT_MODE_TOOLTIP_STYLE = {
561
+ position: "absolute",
562
+ top: "-28px",
563
+ left: "50%",
564
+ transform: "translateX(-50%)",
565
+ fontSize: "10px",
566
+ fontFamily: "system-ui, sans-serif",
567
+ padding: "4px 8px",
568
+ backgroundColor: "rgb(37 99 235)",
569
+ color: "white",
570
+ borderRadius: "4px",
571
+ whiteSpace: "nowrap",
572
+ pointerEvents: "none",
573
+ zIndex: 100,
574
+ boxShadow: "0 1px 3px rgba(0,0,0,0.2)",
575
+ transition: "opacity 0.15s ease"
576
+ };
577
+ function Editable({
578
+ contentKey,
579
+ defaultValue,
580
+ as = "span",
581
+ multiline = false,
582
+ className
583
+ }) {
584
+ const [isHovered, setIsHovered] = useState7(false);
585
+ const isEditMode = useEditModeMessaging();
586
+ const sendEditRequest = useSendEditRequest();
587
+ const { value, isLoading, contentType } = useSiteContentQuery(contentKey, {
588
+ defaultValue
589
+ });
590
+ const section = contentKey.includes(".") ? contentKey.split(".")[0] : void 0;
591
+ const handleClick = useCallback3(
592
+ (e) => {
593
+ if (isEditMode) {
594
+ e.preventDefault();
595
+ e.stopPropagation();
596
+ sendEditRequest(
597
+ contentKey,
598
+ value,
599
+ contentType?.toLowerCase() || "text",
600
+ section
601
+ );
602
+ }
603
+ },
604
+ [isEditMode, contentKey, value, contentType, section, sendEditRequest]
605
+ );
606
+ if (isLoading) {
607
+ return createElement(as, {
608
+ className: cn(
609
+ "animate-pulse bg-muted rounded",
610
+ multiline ? "h-20" : "h-6",
611
+ "inline-block min-w-[100px]",
612
+ className
613
+ ),
614
+ "aria-busy": true,
615
+ "aria-label": "Loading content..."
616
+ });
617
+ }
618
+ const editModeWrapperStyle = isEditMode ? {
619
+ position: "relative",
620
+ display: "inline",
621
+ cursor: "pointer",
622
+ borderRadius: "2px",
623
+ outline: isHovered ? "2px solid rgb(59 130 246)" : "none",
624
+ outlineOffset: "2px",
625
+ backgroundColor: isHovered ? "rgba(59 130 246 / 0.08)" : void 0
626
+ } : {};
627
+ const tooltipSpan = isEditMode ? /* @__PURE__ */ jsx6(
628
+ "span",
629
+ {
630
+ style: {
631
+ ...EDIT_MODE_TOOLTIP_STYLE,
632
+ opacity: isHovered ? 1 : 0
633
+ },
634
+ "aria-hidden": true,
635
+ children: "Click to edit"
636
+ }
637
+ ) : null;
638
+ const hoverHandlers = isEditMode ? {
639
+ onMouseEnter: () => setIsHovered(true),
640
+ onMouseLeave: () => setIsHovered(false)
641
+ } : {};
642
+ if (multiline && value.includes("\n")) {
643
+ const safeBr = sanitizeHtml(value.replace(/\n/g, "<br />"));
644
+ return createElement(
645
+ "span",
646
+ { style: editModeWrapperStyle, ...hoverHandlers },
647
+ createElement(as, {
648
+ className,
649
+ "data-content-key": contentKey,
650
+ "data-editable": isEditMode ? "true" : void 0,
651
+ onClick: isEditMode ? handleClick : void 0,
652
+ dangerouslySetInnerHTML: { __html: safeBr }
653
+ }),
654
+ tooltipSpan
655
+ );
656
+ }
657
+ return createElement(
658
+ "span",
659
+ { style: editModeWrapperStyle, ...hoverHandlers },
660
+ createElement(
661
+ as,
662
+ {
663
+ className,
664
+ "data-content-key": contentKey,
665
+ "data-editable": isEditMode ? "true" : void 0,
666
+ onClick: isEditMode ? handleClick : void 0
667
+ },
668
+ value
669
+ ),
670
+ tooltipSpan
671
+ );
672
+ }
673
+ function EditableHTML({
674
+ contentKey,
675
+ defaultValue,
676
+ as = "div",
677
+ className
678
+ }) {
679
+ const [isHovered, setIsHovered] = useState7(false);
680
+ const isEditMode = useEditModeMessaging();
681
+ const sendEditRequest = useSendEditRequest();
682
+ const { value, isLoading } = useSiteContentQuery(contentKey, {
683
+ defaultValue
684
+ });
685
+ const section = contentKey.includes(".") ? contentKey.split(".")[0] : void 0;
686
+ const handleClick = useCallback3(
687
+ (e) => {
688
+ if (isEditMode) {
689
+ e.preventDefault();
690
+ e.stopPropagation();
691
+ sendEditRequest(contentKey, value, "html", section);
692
+ }
693
+ },
694
+ [isEditMode, contentKey, value, section, sendEditRequest]
695
+ );
696
+ if (isLoading) {
697
+ return createElement(as, {
698
+ className: cn("animate-pulse bg-muted rounded h-32", className),
699
+ "aria-busy": true
700
+ });
701
+ }
702
+ const wrapperStyle = isEditMode ? {
703
+ position: "relative",
704
+ cursor: "pointer",
705
+ borderRadius: "2px",
706
+ outline: isHovered ? "2px solid rgb(59 130 246)" : "none",
707
+ outlineOffset: "2px",
708
+ backgroundColor: isHovered ? "rgba(59 130 246 / 0.08)" : void 0
709
+ } : {};
710
+ const safeHtml = sanitizeHtml(value);
711
+ const hoverHandlers = isEditMode ? { onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false) } : {};
712
+ return createElement(
713
+ "div",
714
+ { style: wrapperStyle, ...hoverHandlers },
715
+ createElement(as, {
716
+ className: cn("prose prose-slate dark:prose-invert", className),
717
+ "data-content-key": contentKey,
718
+ "data-editable": isEditMode ? "true" : void 0,
719
+ onClick: isEditMode ? handleClick : void 0,
720
+ dangerouslySetInnerHTML: { __html: safeHtml }
721
+ }),
722
+ isEditMode && /* @__PURE__ */ jsx6(
723
+ "span",
724
+ {
725
+ style: {
726
+ ...EDIT_MODE_TOOLTIP_STYLE,
727
+ top: "-24px",
728
+ opacity: isHovered ? 1 : 0
729
+ },
730
+ "aria-hidden": true,
731
+ children: "Click to edit"
732
+ }
733
+ )
734
+ );
735
+ }
736
+ function EditableImage({
737
+ contentKey,
738
+ defaultValue,
739
+ alt,
740
+ className,
741
+ width,
742
+ height,
743
+ priority = false
744
+ }) {
745
+ const [isHovered, setIsHovered] = useState7(false);
746
+ const isEditMode = useEditModeMessaging();
747
+ const sendEditRequest = useSendEditRequest();
748
+ const { value: src, isLoading } = useSiteContentQuery(contentKey, {
749
+ defaultValue
750
+ });
751
+ const section = contentKey.includes(".") ? contentKey.split(".")[0] : void 0;
752
+ const handleClick = useCallback3(
753
+ (e) => {
754
+ if (isEditMode) {
755
+ e.preventDefault();
756
+ e.stopPropagation();
757
+ sendEditRequest(contentKey, src, "image", section);
758
+ }
759
+ },
760
+ [isEditMode, contentKey, src, section, sendEditRequest]
761
+ );
762
+ if (isLoading) {
763
+ return /* @__PURE__ */ jsx6(
764
+ "div",
765
+ {
766
+ className: cn("animate-pulse bg-muted rounded", className),
767
+ style: { width, height },
768
+ "aria-busy": "true"
769
+ }
770
+ );
771
+ }
772
+ const wrapperStyle = isEditMode ? {
773
+ position: "relative",
774
+ display: "inline-block",
775
+ cursor: "pointer",
776
+ borderRadius: "2px",
777
+ outline: isHovered ? "2px solid rgb(59 130 246)" : "none",
778
+ outlineOffset: "2px"
779
+ } : {};
780
+ const hoverHandlers = isEditMode ? { onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false) } : {};
781
+ return /* @__PURE__ */ jsxs("div", { style: wrapperStyle, ...hoverHandlers, children: [
782
+ /* @__PURE__ */ jsx6(
783
+ "img",
784
+ {
785
+ src,
786
+ alt,
787
+ className,
788
+ style: isEditMode && isHovered ? { opacity: 0.95 } : void 0,
789
+ width,
790
+ height,
791
+ loading: priority ? "eager" : "lazy",
792
+ "data-content-key": contentKey,
793
+ "data-editable": isEditMode ? "true" : void 0,
794
+ onClick: isEditMode ? handleClick : void 0
795
+ }
796
+ ),
797
+ isEditMode && /* @__PURE__ */ jsx6(
798
+ "span",
799
+ {
800
+ style: {
801
+ ...EDIT_MODE_TOOLTIP_STYLE,
802
+ top: "8px",
803
+ left: "8px",
804
+ transform: "none",
805
+ opacity: isHovered ? 1 : 0
806
+ },
807
+ "aria-hidden": true,
808
+ children: "Click to edit image"
809
+ }
810
+ )
811
+ ] });
812
+ }
813
+
347
814
  // src/hooks/useServices.ts
348
- import { useState as useState5, useEffect as useEffect5 } from "react";
815
+ import { useState as useState8, useEffect as useEffect7 } from "react";
349
816
  function useServices(options = {}) {
350
817
  const { client } = useFoxPixelContext();
351
- const [services, setServices] = useState5(null);
352
- const [isLoading, setIsLoading] = useState5(true);
353
- const [error, setError] = useState5(null);
818
+ const [services, setServices] = useState8(null);
819
+ const [isLoading, setIsLoading] = useState8(true);
820
+ const [error, setError] = useState8(null);
354
821
  const fetchServices = async () => {
355
822
  try {
356
823
  setIsLoading(true);
@@ -368,7 +835,7 @@ function useServices(options = {}) {
368
835
  setIsLoading(false);
369
836
  }
370
837
  };
371
- useEffect5(() => {
838
+ useEffect7(() => {
372
839
  fetchServices();
373
840
  }, [options.category, options.active]);
374
841
  return {
@@ -380,11 +847,11 @@ function useServices(options = {}) {
380
847
  }
381
848
 
382
849
  // src/hooks/useLeadCapture.ts
383
- import { useState as useState6 } from "react";
850
+ import { useState as useState9 } from "react";
384
851
  function useLeadCapture() {
385
852
  const { client } = useFoxPixelContext();
386
- const [isLoading, setIsLoading] = useState6(false);
387
- const [error, setError] = useState6(null);
853
+ const [isLoading, setIsLoading] = useState9(false);
854
+ const [error, setError] = useState9(null);
388
855
  const captureLead = async (data) => {
389
856
  try {
390
857
  setIsLoading(true);
@@ -407,11 +874,11 @@ function useLeadCapture() {
407
874
  }
408
875
 
409
876
  // src/hooks/useContactCapture.ts
410
- import { useState as useState7 } from "react";
877
+ import { useState as useState10 } from "react";
411
878
  function useContactCapture() {
412
879
  const { client } = useFoxPixelContext();
413
- const [isLoading, setIsLoading] = useState7(false);
414
- const [error, setError] = useState7(null);
880
+ const [isLoading, setIsLoading] = useState10(false);
881
+ const [error, setError] = useState10(null);
415
882
  const captureContact = async (data) => {
416
883
  try {
417
884
  setIsLoading(true);
@@ -433,13 +900,174 @@ function useContactCapture() {
433
900
  };
434
901
  }
435
902
 
903
+ // src/hooks/useSiteContent.ts
904
+ import { useState as useState11, useEffect as useEffect8, useCallback as useCallback4 } from "react";
905
+ function useSiteContent(contentKey, options = {}) {
906
+ const { defaultValue = "", fetchOnMount = true } = options;
907
+ const { client } = useFoxPixelContext();
908
+ const { user, hasPermission } = useAuth();
909
+ const [data, setData] = useState11(null);
910
+ const [isLoading, setIsLoading] = useState11(fetchOnMount);
911
+ const [error, setError] = useState11(null);
912
+ const canEdit = user !== null && hasPermission("site:content:update");
913
+ const fetchContent = useCallback4(async () => {
914
+ try {
915
+ setIsLoading(true);
916
+ setError(null);
917
+ const content = await client.get(
918
+ `/api/site/content/${encodeURIComponent(contentKey)}`
919
+ );
920
+ setData(content);
921
+ } catch (err) {
922
+ if (err?.status === 404) {
923
+ setData(null);
924
+ } else {
925
+ setError(err);
926
+ }
927
+ } finally {
928
+ setIsLoading(false);
929
+ }
930
+ }, [client, contentKey]);
931
+ const updateContent = useCallback4(async (newValue) => {
932
+ try {
933
+ setError(null);
934
+ const updated = await client.put(
935
+ `/api/site/content/${encodeURIComponent(contentKey)}`,
936
+ { value: newValue }
937
+ );
938
+ setData(updated);
939
+ } catch (err) {
940
+ setError(err);
941
+ throw err;
942
+ }
943
+ }, [client, contentKey]);
944
+ useEffect8(() => {
945
+ if (fetchOnMount) {
946
+ fetchContent();
947
+ }
948
+ }, [contentKey, fetchOnMount]);
949
+ const value = data?.value ?? defaultValue;
950
+ return {
951
+ data,
952
+ value,
953
+ isLoading,
954
+ error,
955
+ canEdit,
956
+ update: updateContent,
957
+ refetch: fetchContent
958
+ };
959
+ }
960
+ function useSiteContents(contentKeys, options = {}) {
961
+ const { defaults = {} } = options;
962
+ const { client } = useFoxPixelContext();
963
+ const [data, setData] = useState11({});
964
+ const [isLoading, setIsLoading] = useState11(true);
965
+ const [error, setError] = useState11(null);
966
+ const fetchContents = useCallback4(async () => {
967
+ if (contentKeys.length === 0) {
968
+ setData({});
969
+ setIsLoading(false);
970
+ return;
971
+ }
972
+ try {
973
+ setIsLoading(true);
974
+ setError(null);
975
+ const contents = await client.post(
976
+ "/api/site/content/batch",
977
+ contentKeys
978
+ );
979
+ setData(contents);
980
+ } catch (err) {
981
+ setError(err);
982
+ } finally {
983
+ setIsLoading(false);
984
+ }
985
+ }, [client, contentKeys.join(",")]);
986
+ useEffect8(() => {
987
+ fetchContents();
988
+ }, [fetchContents]);
989
+ const getValue = useCallback4((key, defaultValue) => {
990
+ const content = data[key];
991
+ if (content?.value) {
992
+ return content.value;
993
+ }
994
+ return defaultValue ?? defaults[key] ?? "";
995
+ }, [data, defaults]);
996
+ return {
997
+ data,
998
+ getValue,
999
+ isLoading,
1000
+ error,
1001
+ refetch: fetchContents
1002
+ };
1003
+ }
1004
+ function useSiteContentSection(section) {
1005
+ const { client } = useFoxPixelContext();
1006
+ const [contents, setContents] = useState11([]);
1007
+ const [isLoading, setIsLoading] = useState11(true);
1008
+ const [error, setError] = useState11(null);
1009
+ const fetchContents = useCallback4(async () => {
1010
+ try {
1011
+ setIsLoading(true);
1012
+ setError(null);
1013
+ const data = await client.get(
1014
+ `/api/site/content/section/${encodeURIComponent(section)}`
1015
+ );
1016
+ setContents(data);
1017
+ } catch (err) {
1018
+ setError(err);
1019
+ } finally {
1020
+ setIsLoading(false);
1021
+ }
1022
+ }, [client, section]);
1023
+ useEffect8(() => {
1024
+ fetchContents();
1025
+ }, [fetchContents]);
1026
+ return {
1027
+ contents,
1028
+ isLoading,
1029
+ error,
1030
+ refetch: fetchContents
1031
+ };
1032
+ }
1033
+
1034
+ // src/prefetchSiteContent.ts
1035
+ async function prefetchSiteContent(queryClient, options) {
1036
+ const { apiUrl, apiKey, tenantId, contentKeys } = options;
1037
+ const client = new FoxPixelHttpClient({ apiUrl, apiKey, tenantId });
1038
+ await Promise.all(
1039
+ contentKeys.map(async (contentKey) => {
1040
+ try {
1041
+ const content = await client.get(
1042
+ `/api/site/content/${encodeURIComponent(contentKey)}`
1043
+ );
1044
+ queryClient.setQueryData(
1045
+ [SITE_CONTENT_QUERY_KEY, contentKey],
1046
+ {
1047
+ value: content?.value ?? "",
1048
+ contentType: content?.contentType ?? "TEXT"
1049
+ }
1050
+ );
1051
+ } catch (err) {
1052
+ const status = err?.response?.status;
1053
+ if (status === 404) {
1054
+ queryClient.setQueryData([SITE_CONTENT_QUERY_KEY, contentKey], {
1055
+ value: "",
1056
+ contentType: "TEXT"
1057
+ });
1058
+ }
1059
+ }
1060
+ })
1061
+ );
1062
+ }
1063
+
436
1064
  // src/blog/hooks.ts
437
- import { useState as useState8, useEffect as useEffect6 } from "react";
1065
+ import { useState as useState12, useEffect as useEffect9 } from "react";
438
1066
  function useBlogPosts(options = {}) {
439
1067
  const { client } = useFoxPixelContext();
440
- const [data, setData] = useState8(null);
441
- const [isLoading, setIsLoading] = useState8(true);
442
- const [error, setError] = useState8(null);
1068
+ const [data, setData] = useState12(null);
1069
+ const [isLoading, setIsLoading] = useState12(true);
1070
+ const [error, setError] = useState12(null);
443
1071
  const page = options.page ?? 0;
444
1072
  const limit = options.limit ?? 10;
445
1073
  const fetchPosts = async () => {
@@ -459,7 +1087,7 @@ function useBlogPosts(options = {}) {
459
1087
  setIsLoading(false);
460
1088
  }
461
1089
  };
462
- useEffect6(() => {
1090
+ useEffect9(() => {
463
1091
  fetchPosts();
464
1092
  }, [page, limit]);
465
1093
  return {
@@ -471,9 +1099,9 @@ function useBlogPosts(options = {}) {
471
1099
  }
472
1100
  function useBlogPost(slug) {
473
1101
  const { client } = useFoxPixelContext();
474
- const [data, setData] = useState8(null);
475
- const [isLoading, setIsLoading] = useState8(!!slug);
476
- const [error, setError] = useState8(null);
1102
+ const [data, setData] = useState12(null);
1103
+ const [isLoading, setIsLoading] = useState12(!!slug);
1104
+ const [error, setError] = useState12(null);
477
1105
  const fetchPost = async () => {
478
1106
  if (!slug) {
479
1107
  setData(null);
@@ -492,7 +1120,7 @@ function useBlogPost(slug) {
492
1120
  setIsLoading(false);
493
1121
  }
494
1122
  };
495
- useEffect6(() => {
1123
+ useEffect9(() => {
496
1124
  fetchPost();
497
1125
  }, [slug]);
498
1126
  return {
@@ -504,9 +1132,9 @@ function useBlogPost(slug) {
504
1132
  }
505
1133
  function useBlogCategories() {
506
1134
  const { client } = useFoxPixelContext();
507
- const [data, setData] = useState8(null);
508
- const [isLoading, setIsLoading] = useState8(true);
509
- const [error, setError] = useState8(null);
1135
+ const [data, setData] = useState12(null);
1136
+ const [isLoading, setIsLoading] = useState12(true);
1137
+ const [error, setError] = useState12(null);
510
1138
  const fetchCategories = async () => {
511
1139
  try {
512
1140
  setIsLoading(true);
@@ -520,7 +1148,7 @@ function useBlogCategories() {
520
1148
  setIsLoading(false);
521
1149
  }
522
1150
  };
523
- useEffect6(() => {
1151
+ useEffect9(() => {
524
1152
  fetchCategories();
525
1153
  }, []);
526
1154
  return {
@@ -532,9 +1160,9 @@ function useBlogCategories() {
532
1160
  }
533
1161
  function useBlogTags() {
534
1162
  const { client } = useFoxPixelContext();
535
- const [data, setData] = useState8(null);
536
- const [isLoading, setIsLoading] = useState8(true);
537
- const [error, setError] = useState8(null);
1163
+ const [data, setData] = useState12(null);
1164
+ const [isLoading, setIsLoading] = useState12(true);
1165
+ const [error, setError] = useState12(null);
538
1166
  const fetchTags = async () => {
539
1167
  try {
540
1168
  setIsLoading(true);
@@ -548,7 +1176,7 @@ function useBlogTags() {
548
1176
  setIsLoading(false);
549
1177
  }
550
1178
  };
551
- useEffect6(() => {
1179
+ useEffect9(() => {
552
1180
  fetchTags();
553
1181
  }, []);
554
1182
  return {
@@ -560,9 +1188,9 @@ function useBlogTags() {
560
1188
  }
561
1189
  function useBlogComments(slug) {
562
1190
  const { client } = useFoxPixelContext();
563
- const [data, setData] = useState8(null);
564
- const [isLoading, setIsLoading] = useState8(!!slug);
565
- const [error, setError] = useState8(null);
1191
+ const [data, setData] = useState12(null);
1192
+ const [isLoading, setIsLoading] = useState12(!!slug);
1193
+ const [error, setError] = useState12(null);
566
1194
  const fetchComments = async () => {
567
1195
  if (!slug) {
568
1196
  setData(null);
@@ -583,7 +1211,7 @@ function useBlogComments(slug) {
583
1211
  setIsLoading(false);
584
1212
  }
585
1213
  };
586
- useEffect6(() => {
1214
+ useEffect9(() => {
587
1215
  fetchComments();
588
1216
  }, [slug]);
589
1217
  return {
@@ -595,8 +1223,8 @@ function useBlogComments(slug) {
595
1223
  }
596
1224
  function useBlogCommentSubmit(slug) {
597
1225
  const { client } = useFoxPixelContext();
598
- const [isSubmitting, setIsSubmitting] = useState8(false);
599
- const [error, setError] = useState8(null);
1226
+ const [isSubmitting, setIsSubmitting] = useState12(false);
1227
+ const [error, setError] = useState12(null);
600
1228
  const submit = async (payload) => {
601
1229
  if (!slug) return null;
602
1230
  try {
@@ -624,9 +1252,9 @@ function useBlogCommentSubmit(slug) {
624
1252
  }
625
1253
  function useBlogFeaturedPosts(limit = 6) {
626
1254
  const { client } = useFoxPixelContext();
627
- const [data, setData] = useState8(null);
628
- const [isLoading, setIsLoading] = useState8(true);
629
- const [error, setError] = useState8(null);
1255
+ const [data, setData] = useState12(null);
1256
+ const [isLoading, setIsLoading] = useState12(true);
1257
+ const [error, setError] = useState12(null);
630
1258
  const fetchFeatured = async () => {
631
1259
  try {
632
1260
  setIsLoading(true);
@@ -644,7 +1272,7 @@ function useBlogFeaturedPosts(limit = 6) {
644
1272
  setIsLoading(false);
645
1273
  }
646
1274
  };
647
- useEffect6(() => {
1275
+ useEffect9(() => {
648
1276
  fetchFeatured();
649
1277
  }, [limit]);
650
1278
  return {
@@ -656,9 +1284,9 @@ function useBlogFeaturedPosts(limit = 6) {
656
1284
  }
657
1285
  function useNewsletterSubscribe() {
658
1286
  const { client } = useFoxPixelContext();
659
- const [isSubmitting, setIsSubmitting] = useState8(false);
660
- const [error, setError] = useState8(null);
661
- const [success, setSuccess] = useState8(false);
1287
+ const [isSubmitting, setIsSubmitting] = useState12(false);
1288
+ const [error, setError] = useState12(null);
1289
+ const [success, setSuccess] = useState12(false);
662
1290
  const subscribe = async (payload) => {
663
1291
  try {
664
1292
  setIsSubmitting(true);
@@ -691,9 +1319,9 @@ function useNewsletterSubscribe() {
691
1319
  }
692
1320
  function useNewsletterUnsubscribe() {
693
1321
  const { client } = useFoxPixelContext();
694
- const [isSubmitting, setIsSubmitting] = useState8(false);
695
- const [error, setError] = useState8(null);
696
- const [success, setSuccess] = useState8(false);
1322
+ const [isSubmitting, setIsSubmitting] = useState12(false);
1323
+ const [error, setError] = useState12(null);
1324
+ const [success, setSuccess] = useState12(false);
697
1325
  const unsubscribe = async (email) => {
698
1326
  try {
699
1327
  setIsSubmitting(true);
@@ -711,8 +1339,26 @@ function useNewsletterUnsubscribe() {
711
1339
  setIsSubmitting(false);
712
1340
  }
713
1341
  };
1342
+ const unsubscribeByToken = async (token) => {
1343
+ try {
1344
+ setIsSubmitting(true);
1345
+ setError(null);
1346
+ setSuccess(false);
1347
+ await client.get("/api/v1/blog/newsletter/unsubscribe", {
1348
+ params: { token }
1349
+ });
1350
+ setSuccess(true);
1351
+ return true;
1352
+ } catch (err) {
1353
+ setError(err);
1354
+ return false;
1355
+ } finally {
1356
+ setIsSubmitting(false);
1357
+ }
1358
+ };
714
1359
  return {
715
1360
  unsubscribe,
1361
+ unsubscribeByToken,
716
1362
  isSubmitting,
717
1363
  error,
718
1364
  success
@@ -720,15 +1366,15 @@ function useNewsletterUnsubscribe() {
720
1366
  }
721
1367
 
722
1368
  // src/blog/admin-hooks.ts
723
- import { useState as useState9, useEffect as useEffect7, useCallback as useCallback2 } from "react";
1369
+ import { useState as useState13, useEffect as useEffect10, useCallback as useCallback5 } from "react";
724
1370
  function useAdminBlogPosts(options = {}) {
725
1371
  const { client } = useFoxPixelContext();
726
- const [data, setData] = useState9(null);
727
- const [isLoading, setIsLoading] = useState9(true);
728
- const [error, setError] = useState9(null);
1372
+ const [data, setData] = useState13(null);
1373
+ const [isLoading, setIsLoading] = useState13(true);
1374
+ const [error, setError] = useState13(null);
729
1375
  const page = options.page ?? 0;
730
1376
  const size = options.size ?? 20;
731
- const fetchPosts = useCallback2(async () => {
1377
+ const fetchPosts = useCallback5(async () => {
732
1378
  try {
733
1379
  setIsLoading(true);
734
1380
  setError(null);
@@ -743,17 +1389,17 @@ function useAdminBlogPosts(options = {}) {
743
1389
  setIsLoading(false);
744
1390
  }
745
1391
  }, [client, page, size]);
746
- useEffect7(() => {
1392
+ useEffect10(() => {
747
1393
  fetchPosts();
748
1394
  }, [fetchPosts]);
749
1395
  return { data, isLoading, error, refetch: fetchPosts };
750
1396
  }
751
1397
  function useAdminBlogPost(id) {
752
1398
  const { client } = useFoxPixelContext();
753
- const [data, setData] = useState9(null);
754
- const [isLoading, setIsLoading] = useState9(!!id);
755
- const [error, setError] = useState9(null);
756
- const fetchPost = useCallback2(async () => {
1399
+ const [data, setData] = useState13(null);
1400
+ const [isLoading, setIsLoading] = useState13(!!id);
1401
+ const [error, setError] = useState13(null);
1402
+ const fetchPost = useCallback5(async () => {
757
1403
  if (!id) {
758
1404
  setData(null);
759
1405
  setIsLoading(false);
@@ -770,15 +1416,15 @@ function useAdminBlogPost(id) {
770
1416
  setIsLoading(false);
771
1417
  }
772
1418
  }, [client, id]);
773
- useEffect7(() => {
1419
+ useEffect10(() => {
774
1420
  fetchPost();
775
1421
  }, [fetchPost]);
776
1422
  return { data, isLoading, error, refetch: fetchPost };
777
1423
  }
778
1424
  function useAdminBlogPostMutations() {
779
1425
  const { client } = useFoxPixelContext();
780
- const [isLoading, setIsLoading] = useState9(false);
781
- const [error, setError] = useState9(null);
1426
+ const [isLoading, setIsLoading] = useState13(false);
1427
+ const [error, setError] = useState13(null);
782
1428
  const create = async (payload) => {
783
1429
  try {
784
1430
  setIsLoading(true);
@@ -822,10 +1468,10 @@ function useAdminBlogPostMutations() {
822
1468
  }
823
1469
  function useAdminBlogCategories() {
824
1470
  const { client } = useFoxPixelContext();
825
- const [data, setData] = useState9(null);
826
- const [isLoading, setIsLoading] = useState9(true);
827
- const [error, setError] = useState9(null);
828
- const fetchCategories = useCallback2(async () => {
1471
+ const [data, setData] = useState13(null);
1472
+ const [isLoading, setIsLoading] = useState13(true);
1473
+ const [error, setError] = useState13(null);
1474
+ const fetchCategories = useCallback5(async () => {
829
1475
  try {
830
1476
  setIsLoading(true);
831
1477
  setError(null);
@@ -837,7 +1483,7 @@ function useAdminBlogCategories() {
837
1483
  setIsLoading(false);
838
1484
  }
839
1485
  }, [client]);
840
- useEffect7(() => {
1486
+ useEffect10(() => {
841
1487
  fetchCategories();
842
1488
  }, [fetchCategories]);
843
1489
  const create = async (payload) => {
@@ -874,10 +1520,10 @@ function useAdminBlogCategories() {
874
1520
  }
875
1521
  function useAdminBlogTags() {
876
1522
  const { client } = useFoxPixelContext();
877
- const [data, setData] = useState9(null);
878
- const [isLoading, setIsLoading] = useState9(true);
879
- const [error, setError] = useState9(null);
880
- const fetchTags = useCallback2(async () => {
1523
+ const [data, setData] = useState13(null);
1524
+ const [isLoading, setIsLoading] = useState13(true);
1525
+ const [error, setError] = useState13(null);
1526
+ const fetchTags = useCallback5(async () => {
881
1527
  try {
882
1528
  setIsLoading(true);
883
1529
  setError(null);
@@ -889,7 +1535,7 @@ function useAdminBlogTags() {
889
1535
  setIsLoading(false);
890
1536
  }
891
1537
  }, [client]);
892
- useEffect7(() => {
1538
+ useEffect10(() => {
893
1539
  fetchTags();
894
1540
  }, [fetchTags]);
895
1541
  const create = async (payload) => {
@@ -926,11 +1572,11 @@ function useAdminBlogTags() {
926
1572
  }
927
1573
  function useAdminBlogComments(options = {}) {
928
1574
  const { client } = useFoxPixelContext();
929
- const [data, setData] = useState9(null);
930
- const [isLoading, setIsLoading] = useState9(true);
931
- const [error, setError] = useState9(null);
1575
+ const [data, setData] = useState13(null);
1576
+ const [isLoading, setIsLoading] = useState13(true);
1577
+ const [error, setError] = useState13(null);
932
1578
  const { status, postId, page = 0, size = 20 } = options;
933
- const fetchComments = useCallback2(async () => {
1579
+ const fetchComments = useCallback5(async () => {
934
1580
  try {
935
1581
  setIsLoading(true);
936
1582
  setError(null);
@@ -947,7 +1593,7 @@ function useAdminBlogComments(options = {}) {
947
1593
  setIsLoading(false);
948
1594
  }
949
1595
  }, [client, status, postId, page, size]);
950
- useEffect7(() => {
1596
+ useEffect10(() => {
951
1597
  fetchComments();
952
1598
  }, [fetchComments]);
953
1599
  const updateStatus = async (id, newStatus) => {
@@ -974,11 +1620,11 @@ function useAdminBlogComments(options = {}) {
974
1620
  }
975
1621
  function useAdminNewsletterSubscribers(options = {}) {
976
1622
  const { client } = useFoxPixelContext();
977
- const [data, setData] = useState9(null);
978
- const [isLoading, setIsLoading] = useState9(true);
979
- const [error, setError] = useState9(null);
1623
+ const [data, setData] = useState13(null);
1624
+ const [isLoading, setIsLoading] = useState13(true);
1625
+ const [error, setError] = useState13(null);
980
1626
  const { status, page = 0, size = 20 } = options;
981
- const fetchSubscribers = useCallback2(async () => {
1627
+ const fetchSubscribers = useCallback5(async () => {
982
1628
  try {
983
1629
  setIsLoading(true);
984
1630
  setError(null);
@@ -994,7 +1640,7 @@ function useAdminNewsletterSubscribers(options = {}) {
994
1640
  setIsLoading(false);
995
1641
  }
996
1642
  }, [client, status, page, size]);
997
- useEffect7(() => {
1643
+ useEffect10(() => {
998
1644
  fetchSubscribers();
999
1645
  }, [fetchSubscribers]);
1000
1646
  const remove = async (id) => {
@@ -1011,10 +1657,10 @@ function useAdminNewsletterSubscribers(options = {}) {
1011
1657
  }
1012
1658
  function useAdminNewsletterStats() {
1013
1659
  const { client } = useFoxPixelContext();
1014
- const [data, setData] = useState9(null);
1015
- const [isLoading, setIsLoading] = useState9(true);
1016
- const [error, setError] = useState9(null);
1017
- const fetchStats = useCallback2(async () => {
1660
+ const [data, setData] = useState13(null);
1661
+ const [isLoading, setIsLoading] = useState13(true);
1662
+ const [error, setError] = useState13(null);
1663
+ const fetchStats = useCallback5(async () => {
1018
1664
  try {
1019
1665
  setIsLoading(true);
1020
1666
  setError(null);
@@ -1026,17 +1672,17 @@ function useAdminNewsletterStats() {
1026
1672
  setIsLoading(false);
1027
1673
  }
1028
1674
  }, [client]);
1029
- useEffect7(() => {
1675
+ useEffect10(() => {
1030
1676
  fetchStats();
1031
1677
  }, [fetchStats]);
1032
1678
  return { data, isLoading, error, refetch: fetchStats };
1033
1679
  }
1034
1680
  function useAdminBlogSettings() {
1035
1681
  const { client } = useFoxPixelContext();
1036
- const [data, setData] = useState9(null);
1037
- const [isLoading, setIsLoading] = useState9(true);
1038
- const [error, setError] = useState9(null);
1039
- const fetchSettings = useCallback2(async () => {
1682
+ const [data, setData] = useState13(null);
1683
+ const [isLoading, setIsLoading] = useState13(true);
1684
+ const [error, setError] = useState13(null);
1685
+ const fetchSettings = useCallback5(async () => {
1040
1686
  try {
1041
1687
  setIsLoading(true);
1042
1688
  setError(null);
@@ -1048,7 +1694,7 @@ function useAdminBlogSettings() {
1048
1694
  setIsLoading(false);
1049
1695
  }
1050
1696
  }, [client]);
1051
- useEffect7(() => {
1697
+ useEffect10(() => {
1052
1698
  fetchSettings();
1053
1699
  }, [fetchSettings]);
1054
1700
  const update = async (settings) => {
@@ -1065,10 +1711,10 @@ function useAdminBlogSettings() {
1065
1711
  }
1066
1712
  function useAdminBlogAnalytics() {
1067
1713
  const { client } = useFoxPixelContext();
1068
- const [data, setData] = useState9(null);
1069
- const [isLoading, setIsLoading] = useState9(true);
1070
- const [error, setError] = useState9(null);
1071
- const fetchAnalytics = useCallback2(async () => {
1714
+ const [data, setData] = useState13(null);
1715
+ const [isLoading, setIsLoading] = useState13(true);
1716
+ const [error, setError] = useState13(null);
1717
+ const fetchAnalytics = useCallback5(async () => {
1072
1718
  try {
1073
1719
  setIsLoading(true);
1074
1720
  setError(null);
@@ -1080,7 +1726,7 @@ function useAdminBlogAnalytics() {
1080
1726
  setIsLoading(false);
1081
1727
  }
1082
1728
  }, [client]);
1083
- useEffect7(() => {
1729
+ useEffect10(() => {
1084
1730
  fetchAnalytics();
1085
1731
  }, [fetchAnalytics]);
1086
1732
  return { data, isLoading, error, refetch: fetchAnalytics };
@@ -1119,11 +1765,16 @@ function getBlogPostSchemaLd(post, options) {
1119
1765
  }
1120
1766
  export {
1121
1767
  AuthProvider,
1768
+ Editable,
1769
+ EditableHTML,
1770
+ EditableImage,
1122
1771
  FoxPixelHttpClient,
1123
1772
  FoxPixelProvider,
1124
1773
  GuestOnlyRoute,
1125
1774
  ProtectedRoute,
1775
+ SITE_CONTENT_QUERY_KEY,
1126
1776
  getBlogPostSchemaLd,
1777
+ prefetchSiteContent,
1127
1778
  useAdminBlogAnalytics,
1128
1779
  useAdminBlogCategories,
1129
1780
  useAdminBlogComments,
@@ -1143,11 +1794,18 @@ export {
1143
1794
  useBlogPosts,
1144
1795
  useBlogTags,
1145
1796
  useContactCapture,
1797
+ useEditMode,
1798
+ useEditModeMessaging,
1146
1799
  useFoxPixelContext,
1147
1800
  useLeadCapture,
1148
1801
  useNewsletterSubscribe,
1149
1802
  useNewsletterUnsubscribe,
1803
+ useSendEditRequest,
1150
1804
  useServices,
1805
+ useSiteContent,
1806
+ useSiteContentQuery,
1807
+ useSiteContentSection,
1808
+ useSiteContents,
1151
1809
  withAuth
1152
1810
  };
1153
1811
  //# sourceMappingURL=index.mjs.map