@foxpixel/react 0.1.0 → 0.2.0

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
@@ -238,7 +238,8 @@ function AuthProvider({
238
238
  logout,
239
239
  register,
240
240
  updateProfile,
241
- refetch: fetchCurrentUser
241
+ refetch: fetchCurrentUser,
242
+ hasPermission: (permission) => user !== null && permission === "site:content:update"
242
243
  };
243
244
  return /* @__PURE__ */ jsx2(AuthContext.Provider, { value, children });
244
245
  }
@@ -344,13 +345,332 @@ function withAuth(Component, options = {}) {
344
345
  };
345
346
  }
346
347
 
348
+ // src/components/Editable.tsx
349
+ import { createElement, useCallback as useCallback3 } from "react";
350
+
351
+ // src/hooks/useEditMode.ts
352
+ import { useEffect as useEffect5, useState as useState5, useCallback as useCallback2 } from "react";
353
+ import { useQueryClient } from "@tanstack/react-query";
354
+ var SITE_CONTENT_QUERY_KEY = "siteContent";
355
+ function useEditMode() {
356
+ const [isEditMode, setIsEditMode] = useState5(false);
357
+ useEffect5(() => {
358
+ if (typeof window === "undefined") return;
359
+ const params = new URLSearchParams(window.location.search);
360
+ setIsEditMode(params.get("edit-mode") === "true");
361
+ }, []);
362
+ return isEditMode;
363
+ }
364
+ function useEditModeMessaging() {
365
+ const queryClient = useQueryClient();
366
+ const isEditMode = useEditMode();
367
+ useEffect5(() => {
368
+ if (!isEditMode || typeof window === "undefined") return;
369
+ window.parent.postMessage({ type: "FOXPIXEL_READY" }, "*");
370
+ const handleMessage = (event) => {
371
+ 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
+ });
376
+ }
377
+ };
378
+ window.addEventListener("message", handleMessage);
379
+ return () => window.removeEventListener("message", handleMessage);
380
+ }, [isEditMode, queryClient]);
381
+ return isEditMode;
382
+ }
383
+ function useSendEditRequest() {
384
+ const isEditMode = useEditMode();
385
+ return useCallback2(
386
+ (contentKey, currentValue, contentType = "text", section, description) => {
387
+ if (!isEditMode) return;
388
+ if (typeof window !== "undefined" && window.parent !== window) {
389
+ window.parent.postMessage(
390
+ {
391
+ type: "FOXPIXEL_EDIT_CONTENT",
392
+ payload: {
393
+ contentKey,
394
+ currentValue,
395
+ contentType,
396
+ section,
397
+ description
398
+ }
399
+ },
400
+ "*"
401
+ );
402
+ }
403
+ },
404
+ [isEditMode]
405
+ );
406
+ }
407
+
408
+ // src/hooks/useSiteContentQuery.ts
409
+ import { useQuery } from "@tanstack/react-query";
410
+ function useSiteContentQuery(contentKey, options) {
411
+ const { defaultValue } = options;
412
+ const { client } = useFoxPixelContext();
413
+ const { data, isLoading } = useQuery({
414
+ queryKey: [SITE_CONTENT_QUERY_KEY, contentKey],
415
+ queryFn: async () => {
416
+ try {
417
+ const content = await client.get(
418
+ `/api/site/content/${encodeURIComponent(contentKey)}`
419
+ );
420
+ if (!content) return null;
421
+ return {
422
+ value: content.value ?? "",
423
+ contentType: content.contentType ?? "TEXT"
424
+ };
425
+ } catch (err) {
426
+ const status = err?.status;
427
+ if (status === 404) return null;
428
+ throw err;
429
+ }
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
+ };
439
+ }
440
+
441
+ // src/utils/sanitize.ts
442
+ import DOMPurify from "isomorphic-dompurify";
443
+ var DEFAULT_ALLOWED_TAGS = [
444
+ "p",
445
+ "br",
446
+ "strong",
447
+ "em",
448
+ "u",
449
+ "s",
450
+ "a",
451
+ "ul",
452
+ "ol",
453
+ "li",
454
+ "h1",
455
+ "h2",
456
+ "h3",
457
+ "h4",
458
+ "h5",
459
+ "h6",
460
+ "blockquote",
461
+ "code",
462
+ "pre",
463
+ "span",
464
+ "div",
465
+ "img",
466
+ "table",
467
+ "thead",
468
+ "tbody",
469
+ "tr",
470
+ "th",
471
+ "td"
472
+ ];
473
+ var DEFAULT_ALLOWED_ATTR = ["href", "target", "rel", "src", "alt", "title", "class"];
474
+ function sanitizeHtml(html) {
475
+ 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"]
480
+ });
481
+ }
482
+
483
+ // src/utils/cn.ts
484
+ function cn(...args) {
485
+ return args.filter(Boolean).join(" ");
486
+ }
487
+
488
+ // src/components/Editable.tsx
489
+ import { Fragment as Fragment3, jsx as jsx6, jsxs } from "react/jsx-runtime";
490
+ function Editable({
491
+ contentKey,
492
+ defaultValue,
493
+ as = "span",
494
+ multiline = false,
495
+ className
496
+ }) {
497
+ const isEditMode = useEditModeMessaging();
498
+ const sendEditRequest = useSendEditRequest();
499
+ const { value, isLoading, contentType } = useSiteContentQuery(contentKey, {
500
+ defaultValue
501
+ });
502
+ const section = contentKey.includes(".") ? contentKey.split(".")[0] : void 0;
503
+ const handleClick = useCallback3(
504
+ (e) => {
505
+ if (isEditMode) {
506
+ e.preventDefault();
507
+ e.stopPropagation();
508
+ sendEditRequest(
509
+ contentKey,
510
+ value,
511
+ contentType?.toLowerCase() || "text",
512
+ section
513
+ );
514
+ }
515
+ },
516
+ [isEditMode, contentKey, value, contentType, section, sendEditRequest]
517
+ );
518
+ if (isLoading) {
519
+ return createElement(as, {
520
+ className: cn(
521
+ "animate-pulse bg-muted rounded",
522
+ multiline ? "h-20" : "h-6",
523
+ "inline-block min-w-[100px]",
524
+ className
525
+ ),
526
+ "aria-busy": true,
527
+ "aria-label": "Loading content..."
528
+ });
529
+ }
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
+ ) : "";
536
+ if (multiline && value.includes("\n")) {
537
+ 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
+ });
546
+ }
547
+ 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
+ ] })
560
+ );
561
+ }
562
+ function EditableHTML({
563
+ contentKey,
564
+ defaultValue,
565
+ as = "div",
566
+ className
567
+ }) {
568
+ const isEditMode = useEditModeMessaging();
569
+ const sendEditRequest = useSendEditRequest();
570
+ const { value, isLoading } = useSiteContentQuery(contentKey, {
571
+ defaultValue
572
+ });
573
+ const section = contentKey.includes(".") ? contentKey.split(".")[0] : void 0;
574
+ const handleClick = useCallback3(
575
+ (e) => {
576
+ if (isEditMode) {
577
+ e.preventDefault();
578
+ e.stopPropagation();
579
+ sendEditRequest(contentKey, value, "html", section);
580
+ }
581
+ },
582
+ [isEditMode, contentKey, value, section, sendEditRequest]
583
+ );
584
+ if (isLoading) {
585
+ return createElement(as, {
586
+ className: cn("animate-pulse bg-muted rounded h-32", className),
587
+ "aria-busy": true
588
+ });
589
+ }
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
+ ) : "";
596
+ 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
+ });
605
+ }
606
+ function EditableImage({
607
+ contentKey,
608
+ defaultValue,
609
+ alt,
610
+ className,
611
+ width,
612
+ height,
613
+ priority = false
614
+ }) {
615
+ const isEditMode = useEditModeMessaging();
616
+ const sendEditRequest = useSendEditRequest();
617
+ const { value: src, isLoading } = useSiteContentQuery(contentKey, {
618
+ defaultValue
619
+ });
620
+ const section = contentKey.includes(".") ? contentKey.split(".")[0] : void 0;
621
+ const handleClick = useCallback3(
622
+ (e) => {
623
+ if (isEditMode) {
624
+ e.preventDefault();
625
+ e.stopPropagation();
626
+ sendEditRequest(contentKey, src, "image", section);
627
+ }
628
+ },
629
+ [isEditMode, contentKey, src, section, sendEditRequest]
630
+ );
631
+ if (isLoading) {
632
+ return /* @__PURE__ */ jsx6(
633
+ "div",
634
+ {
635
+ className: cn("animate-pulse bg-muted rounded", className),
636
+ style: { width, height },
637
+ "aria-busy": "true"
638
+ }
639
+ );
640
+ }
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: [
648
+ /* @__PURE__ */ jsx6(
649
+ "img",
650
+ {
651
+ src,
652
+ alt,
653
+ className: cn(className, editModeStyles),
654
+ width,
655
+ height,
656
+ loading: priority ? "eager" : "lazy",
657
+ "data-content-key": contentKey,
658
+ "data-editable": isEditMode ? "true" : void 0,
659
+ onClick: isEditMode ? handleClick : void 0,
660
+ title: isEditMode ? "Click to edit image" : void 0
661
+ }
662
+ ),
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" })
664
+ ] });
665
+ }
666
+
347
667
  // src/hooks/useServices.ts
348
- import { useState as useState5, useEffect as useEffect5 } from "react";
668
+ import { useState as useState6, useEffect as useEffect6 } from "react";
349
669
  function useServices(options = {}) {
350
670
  const { client } = useFoxPixelContext();
351
- const [services, setServices] = useState5(null);
352
- const [isLoading, setIsLoading] = useState5(true);
353
- const [error, setError] = useState5(null);
671
+ const [services, setServices] = useState6(null);
672
+ const [isLoading, setIsLoading] = useState6(true);
673
+ const [error, setError] = useState6(null);
354
674
  const fetchServices = async () => {
355
675
  try {
356
676
  setIsLoading(true);
@@ -368,7 +688,7 @@ function useServices(options = {}) {
368
688
  setIsLoading(false);
369
689
  }
370
690
  };
371
- useEffect5(() => {
691
+ useEffect6(() => {
372
692
  fetchServices();
373
693
  }, [options.category, options.active]);
374
694
  return {
@@ -380,11 +700,11 @@ function useServices(options = {}) {
380
700
  }
381
701
 
382
702
  // src/hooks/useLeadCapture.ts
383
- import { useState as useState6 } from "react";
703
+ import { useState as useState7 } from "react";
384
704
  function useLeadCapture() {
385
705
  const { client } = useFoxPixelContext();
386
- const [isLoading, setIsLoading] = useState6(false);
387
- const [error, setError] = useState6(null);
706
+ const [isLoading, setIsLoading] = useState7(false);
707
+ const [error, setError] = useState7(null);
388
708
  const captureLead = async (data) => {
389
709
  try {
390
710
  setIsLoading(true);
@@ -407,11 +727,11 @@ function useLeadCapture() {
407
727
  }
408
728
 
409
729
  // src/hooks/useContactCapture.ts
410
- import { useState as useState7 } from "react";
730
+ import { useState as useState8 } from "react";
411
731
  function useContactCapture() {
412
732
  const { client } = useFoxPixelContext();
413
- const [isLoading, setIsLoading] = useState7(false);
414
- const [error, setError] = useState7(null);
733
+ const [isLoading, setIsLoading] = useState8(false);
734
+ const [error, setError] = useState8(null);
415
735
  const captureContact = async (data) => {
416
736
  try {
417
737
  setIsLoading(true);
@@ -432,17 +752,882 @@ function useContactCapture() {
432
752
  error
433
753
  };
434
754
  }
755
+
756
+ // src/hooks/useSiteContent.ts
757
+ import { useState as useState9, useEffect as useEffect7, useCallback as useCallback4 } from "react";
758
+ function useSiteContent(contentKey, options = {}) {
759
+ const { defaultValue = "", fetchOnMount = true } = options;
760
+ const { client } = useFoxPixelContext();
761
+ const { user, hasPermission } = useAuth();
762
+ const [data, setData] = useState9(null);
763
+ const [isLoading, setIsLoading] = useState9(fetchOnMount);
764
+ const [error, setError] = useState9(null);
765
+ const canEdit = user !== null && hasPermission("site:content:update");
766
+ const fetchContent = useCallback4(async () => {
767
+ try {
768
+ setIsLoading(true);
769
+ setError(null);
770
+ const content = await client.get(
771
+ `/api/site/content/${encodeURIComponent(contentKey)}`
772
+ );
773
+ setData(content);
774
+ } catch (err) {
775
+ if (err?.status === 404) {
776
+ setData(null);
777
+ } else {
778
+ setError(err);
779
+ }
780
+ } finally {
781
+ setIsLoading(false);
782
+ }
783
+ }, [client, contentKey]);
784
+ const updateContent = useCallback4(async (newValue) => {
785
+ try {
786
+ setError(null);
787
+ const updated = await client.put(
788
+ `/api/site/content/${encodeURIComponent(contentKey)}`,
789
+ { value: newValue }
790
+ );
791
+ setData(updated);
792
+ } catch (err) {
793
+ setError(err);
794
+ throw err;
795
+ }
796
+ }, [client, contentKey]);
797
+ useEffect7(() => {
798
+ if (fetchOnMount) {
799
+ fetchContent();
800
+ }
801
+ }, [contentKey, fetchOnMount]);
802
+ const value = data?.value ?? defaultValue;
803
+ return {
804
+ data,
805
+ value,
806
+ isLoading,
807
+ error,
808
+ canEdit,
809
+ update: updateContent,
810
+ refetch: fetchContent
811
+ };
812
+ }
813
+ function useSiteContents(contentKeys, options = {}) {
814
+ const { defaults = {} } = options;
815
+ const { client } = useFoxPixelContext();
816
+ const [data, setData] = useState9({});
817
+ const [isLoading, setIsLoading] = useState9(true);
818
+ const [error, setError] = useState9(null);
819
+ const fetchContents = useCallback4(async () => {
820
+ if (contentKeys.length === 0) {
821
+ setData({});
822
+ setIsLoading(false);
823
+ return;
824
+ }
825
+ try {
826
+ setIsLoading(true);
827
+ setError(null);
828
+ const contents = await client.post(
829
+ "/api/site/content/batch",
830
+ contentKeys
831
+ );
832
+ setData(contents);
833
+ } catch (err) {
834
+ setError(err);
835
+ } finally {
836
+ setIsLoading(false);
837
+ }
838
+ }, [client, contentKeys.join(",")]);
839
+ useEffect7(() => {
840
+ fetchContents();
841
+ }, [fetchContents]);
842
+ const getValue = useCallback4((key, defaultValue) => {
843
+ const content = data[key];
844
+ if (content?.value) {
845
+ return content.value;
846
+ }
847
+ return defaultValue ?? defaults[key] ?? "";
848
+ }, [data, defaults]);
849
+ return {
850
+ data,
851
+ getValue,
852
+ isLoading,
853
+ error,
854
+ refetch: fetchContents
855
+ };
856
+ }
857
+ function useSiteContentSection(section) {
858
+ const { client } = useFoxPixelContext();
859
+ const [contents, setContents] = useState9([]);
860
+ const [isLoading, setIsLoading] = useState9(true);
861
+ const [error, setError] = useState9(null);
862
+ const fetchContents = useCallback4(async () => {
863
+ try {
864
+ setIsLoading(true);
865
+ setError(null);
866
+ const data = await client.get(
867
+ `/api/site/content/section/${encodeURIComponent(section)}`
868
+ );
869
+ setContents(data);
870
+ } catch (err) {
871
+ setError(err);
872
+ } finally {
873
+ setIsLoading(false);
874
+ }
875
+ }, [client, section]);
876
+ useEffect7(() => {
877
+ fetchContents();
878
+ }, [fetchContents]);
879
+ return {
880
+ contents,
881
+ isLoading,
882
+ error,
883
+ refetch: fetchContents
884
+ };
885
+ }
886
+
887
+ // src/blog/hooks.ts
888
+ import { useState as useState10, useEffect as useEffect8 } from "react";
889
+ function useBlogPosts(options = {}) {
890
+ const { client } = useFoxPixelContext();
891
+ const [data, setData] = useState10(null);
892
+ const [isLoading, setIsLoading] = useState10(true);
893
+ const [error, setError] = useState10(null);
894
+ const page = options.page ?? 0;
895
+ const limit = options.limit ?? 10;
896
+ const fetchPosts = async () => {
897
+ try {
898
+ setIsLoading(true);
899
+ setError(null);
900
+ const params = new URLSearchParams();
901
+ params.append("page", String(page));
902
+ params.append("size", String(limit));
903
+ const url = `/api/v1/blog/posts?${params.toString()}`;
904
+ const result = await client.get(url);
905
+ setData(result);
906
+ } catch (err) {
907
+ setError(err);
908
+ setData(null);
909
+ } finally {
910
+ setIsLoading(false);
911
+ }
912
+ };
913
+ useEffect8(() => {
914
+ fetchPosts();
915
+ }, [page, limit]);
916
+ return {
917
+ data,
918
+ isLoading,
919
+ error,
920
+ refetch: fetchPosts
921
+ };
922
+ }
923
+ function useBlogPost(slug) {
924
+ const { client } = useFoxPixelContext();
925
+ const [data, setData] = useState10(null);
926
+ const [isLoading, setIsLoading] = useState10(!!slug);
927
+ const [error, setError] = useState10(null);
928
+ const fetchPost = async () => {
929
+ if (!slug) {
930
+ setData(null);
931
+ setIsLoading(false);
932
+ return;
933
+ }
934
+ try {
935
+ setIsLoading(true);
936
+ setError(null);
937
+ const result = await client.get(`/api/v1/blog/posts/${encodeURIComponent(slug)}`);
938
+ setData(result);
939
+ } catch (err) {
940
+ setError(err);
941
+ setData(null);
942
+ } finally {
943
+ setIsLoading(false);
944
+ }
945
+ };
946
+ useEffect8(() => {
947
+ fetchPost();
948
+ }, [slug]);
949
+ return {
950
+ data,
951
+ isLoading,
952
+ error,
953
+ refetch: fetchPost
954
+ };
955
+ }
956
+ function useBlogCategories() {
957
+ const { client } = useFoxPixelContext();
958
+ const [data, setData] = useState10(null);
959
+ const [isLoading, setIsLoading] = useState10(true);
960
+ const [error, setError] = useState10(null);
961
+ const fetchCategories = async () => {
962
+ try {
963
+ setIsLoading(true);
964
+ setError(null);
965
+ const result = await client.get("/api/v1/blog/categories");
966
+ setData(Array.isArray(result) ? result : []);
967
+ } catch (err) {
968
+ setError(err);
969
+ setData(null);
970
+ } finally {
971
+ setIsLoading(false);
972
+ }
973
+ };
974
+ useEffect8(() => {
975
+ fetchCategories();
976
+ }, []);
977
+ return {
978
+ data,
979
+ isLoading,
980
+ error,
981
+ refetch: fetchCategories
982
+ };
983
+ }
984
+ function useBlogTags() {
985
+ const { client } = useFoxPixelContext();
986
+ const [data, setData] = useState10(null);
987
+ const [isLoading, setIsLoading] = useState10(true);
988
+ const [error, setError] = useState10(null);
989
+ const fetchTags = async () => {
990
+ try {
991
+ setIsLoading(true);
992
+ setError(null);
993
+ const result = await client.get("/api/v1/blog/tags");
994
+ setData(Array.isArray(result) ? result : []);
995
+ } catch (err) {
996
+ setError(err);
997
+ setData(null);
998
+ } finally {
999
+ setIsLoading(false);
1000
+ }
1001
+ };
1002
+ useEffect8(() => {
1003
+ fetchTags();
1004
+ }, []);
1005
+ return {
1006
+ data,
1007
+ isLoading,
1008
+ error,
1009
+ refetch: fetchTags
1010
+ };
1011
+ }
1012
+ function useBlogComments(slug) {
1013
+ const { client } = useFoxPixelContext();
1014
+ const [data, setData] = useState10(null);
1015
+ const [isLoading, setIsLoading] = useState10(!!slug);
1016
+ const [error, setError] = useState10(null);
1017
+ const fetchComments = async () => {
1018
+ if (!slug) {
1019
+ setData(null);
1020
+ setIsLoading(false);
1021
+ return;
1022
+ }
1023
+ try {
1024
+ setIsLoading(true);
1025
+ setError(null);
1026
+ const result = await client.get(
1027
+ `/api/v1/blog/posts/${encodeURIComponent(slug)}/comments`
1028
+ );
1029
+ setData(Array.isArray(result) ? result : []);
1030
+ } catch (err) {
1031
+ setError(err);
1032
+ setData(null);
1033
+ } finally {
1034
+ setIsLoading(false);
1035
+ }
1036
+ };
1037
+ useEffect8(() => {
1038
+ fetchComments();
1039
+ }, [slug]);
1040
+ return {
1041
+ data,
1042
+ isLoading,
1043
+ error,
1044
+ refetch: fetchComments
1045
+ };
1046
+ }
1047
+ function useBlogCommentSubmit(slug) {
1048
+ const { client } = useFoxPixelContext();
1049
+ const [isSubmitting, setIsSubmitting] = useState10(false);
1050
+ const [error, setError] = useState10(null);
1051
+ const submit = async (payload) => {
1052
+ if (!slug) return null;
1053
+ try {
1054
+ setIsSubmitting(true);
1055
+ setError(null);
1056
+ const result = await client.post(
1057
+ `/api/v1/blog/posts/${encodeURIComponent(slug)}/comments`,
1058
+ payload
1059
+ );
1060
+ return result;
1061
+ } catch (err) {
1062
+ setError(err);
1063
+ return null;
1064
+ } finally {
1065
+ setIsSubmitting(false);
1066
+ }
1067
+ };
1068
+ const resetError = () => setError(null);
1069
+ return {
1070
+ submit,
1071
+ isSubmitting,
1072
+ error,
1073
+ resetError
1074
+ };
1075
+ }
1076
+ function useBlogFeaturedPosts(limit = 6) {
1077
+ const { client } = useFoxPixelContext();
1078
+ const [data, setData] = useState10(null);
1079
+ const [isLoading, setIsLoading] = useState10(true);
1080
+ const [error, setError] = useState10(null);
1081
+ const fetchFeatured = async () => {
1082
+ try {
1083
+ setIsLoading(true);
1084
+ setError(null);
1085
+ const params = new URLSearchParams();
1086
+ params.append("page", "0");
1087
+ params.append("size", String(limit));
1088
+ const url = `/api/v1/blog/posts/featured?${params.toString()}`;
1089
+ const result = await client.get(url);
1090
+ setData(result);
1091
+ } catch (err) {
1092
+ setError(err);
1093
+ setData(null);
1094
+ } finally {
1095
+ setIsLoading(false);
1096
+ }
1097
+ };
1098
+ useEffect8(() => {
1099
+ fetchFeatured();
1100
+ }, [limit]);
1101
+ return {
1102
+ data,
1103
+ isLoading,
1104
+ error,
1105
+ refetch: fetchFeatured
1106
+ };
1107
+ }
1108
+ function useNewsletterSubscribe() {
1109
+ const { client } = useFoxPixelContext();
1110
+ const [isSubmitting, setIsSubmitting] = useState10(false);
1111
+ const [error, setError] = useState10(null);
1112
+ const [success, setSuccess] = useState10(false);
1113
+ const subscribe = async (payload) => {
1114
+ try {
1115
+ setIsSubmitting(true);
1116
+ setError(null);
1117
+ setSuccess(false);
1118
+ const result = await client.post(
1119
+ "/api/v1/blog/newsletter/subscribe",
1120
+ payload
1121
+ );
1122
+ setSuccess(true);
1123
+ return result;
1124
+ } catch (err) {
1125
+ setError(err);
1126
+ return null;
1127
+ } finally {
1128
+ setIsSubmitting(false);
1129
+ }
1130
+ };
1131
+ const reset = () => {
1132
+ setError(null);
1133
+ setSuccess(false);
1134
+ };
1135
+ return {
1136
+ subscribe,
1137
+ isSubmitting,
1138
+ error,
1139
+ success,
1140
+ reset
1141
+ };
1142
+ }
1143
+ function useNewsletterUnsubscribe() {
1144
+ const { client } = useFoxPixelContext();
1145
+ const [isSubmitting, setIsSubmitting] = useState10(false);
1146
+ const [error, setError] = useState10(null);
1147
+ const [success, setSuccess] = useState10(false);
1148
+ const unsubscribe = async (email) => {
1149
+ try {
1150
+ setIsSubmitting(true);
1151
+ setError(null);
1152
+ setSuccess(false);
1153
+ await client.post("/api/v1/blog/newsletter/unsubscribe", null, {
1154
+ params: { email }
1155
+ });
1156
+ setSuccess(true);
1157
+ return true;
1158
+ } catch (err) {
1159
+ setError(err);
1160
+ return false;
1161
+ } finally {
1162
+ setIsSubmitting(false);
1163
+ }
1164
+ };
1165
+ const unsubscribeByToken = async (token) => {
1166
+ try {
1167
+ setIsSubmitting(true);
1168
+ setError(null);
1169
+ setSuccess(false);
1170
+ await client.get("/api/v1/blog/newsletter/unsubscribe", {
1171
+ params: { token }
1172
+ });
1173
+ setSuccess(true);
1174
+ return true;
1175
+ } catch (err) {
1176
+ setError(err);
1177
+ return false;
1178
+ } finally {
1179
+ setIsSubmitting(false);
1180
+ }
1181
+ };
1182
+ return {
1183
+ unsubscribe,
1184
+ unsubscribeByToken,
1185
+ isSubmitting,
1186
+ error,
1187
+ success
1188
+ };
1189
+ }
1190
+
1191
+ // src/blog/admin-hooks.ts
1192
+ import { useState as useState11, useEffect as useEffect9, useCallback as useCallback5 } from "react";
1193
+ function useAdminBlogPosts(options = {}) {
1194
+ const { client } = useFoxPixelContext();
1195
+ const [data, setData] = useState11(null);
1196
+ const [isLoading, setIsLoading] = useState11(true);
1197
+ const [error, setError] = useState11(null);
1198
+ const page = options.page ?? 0;
1199
+ const size = options.size ?? 20;
1200
+ const fetchPosts = useCallback5(async () => {
1201
+ try {
1202
+ setIsLoading(true);
1203
+ setError(null);
1204
+ const params = new URLSearchParams();
1205
+ params.append("page", String(page));
1206
+ params.append("size", String(size));
1207
+ const result = await client.get(`/api/blog/posts?${params.toString()}`);
1208
+ setData(result);
1209
+ } catch (err) {
1210
+ setError(err);
1211
+ } finally {
1212
+ setIsLoading(false);
1213
+ }
1214
+ }, [client, page, size]);
1215
+ useEffect9(() => {
1216
+ fetchPosts();
1217
+ }, [fetchPosts]);
1218
+ return { data, isLoading, error, refetch: fetchPosts };
1219
+ }
1220
+ function useAdminBlogPost(id) {
1221
+ const { client } = useFoxPixelContext();
1222
+ const [data, setData] = useState11(null);
1223
+ const [isLoading, setIsLoading] = useState11(!!id);
1224
+ const [error, setError] = useState11(null);
1225
+ const fetchPost = useCallback5(async () => {
1226
+ if (!id) {
1227
+ setData(null);
1228
+ setIsLoading(false);
1229
+ return;
1230
+ }
1231
+ try {
1232
+ setIsLoading(true);
1233
+ setError(null);
1234
+ const result = await client.get(`/api/blog/posts/${id}`);
1235
+ setData(result);
1236
+ } catch (err) {
1237
+ setError(err);
1238
+ } finally {
1239
+ setIsLoading(false);
1240
+ }
1241
+ }, [client, id]);
1242
+ useEffect9(() => {
1243
+ fetchPost();
1244
+ }, [fetchPost]);
1245
+ return { data, isLoading, error, refetch: fetchPost };
1246
+ }
1247
+ function useAdminBlogPostMutations() {
1248
+ const { client } = useFoxPixelContext();
1249
+ const [isLoading, setIsLoading] = useState11(false);
1250
+ const [error, setError] = useState11(null);
1251
+ const create = async (payload) => {
1252
+ try {
1253
+ setIsLoading(true);
1254
+ setError(null);
1255
+ const result = await client.post("/api/blog/posts", payload);
1256
+ return result;
1257
+ } catch (err) {
1258
+ setError(err);
1259
+ return null;
1260
+ } finally {
1261
+ setIsLoading(false);
1262
+ }
1263
+ };
1264
+ const update = async (id, payload) => {
1265
+ try {
1266
+ setIsLoading(true);
1267
+ setError(null);
1268
+ const result = await client.put(`/api/blog/posts/${id}`, payload);
1269
+ return result;
1270
+ } catch (err) {
1271
+ setError(err);
1272
+ return null;
1273
+ } finally {
1274
+ setIsLoading(false);
1275
+ }
1276
+ };
1277
+ const remove = async (id) => {
1278
+ try {
1279
+ setIsLoading(true);
1280
+ setError(null);
1281
+ await client.delete(`/api/blog/posts/${id}`);
1282
+ return true;
1283
+ } catch (err) {
1284
+ setError(err);
1285
+ return false;
1286
+ } finally {
1287
+ setIsLoading(false);
1288
+ }
1289
+ };
1290
+ return { create, update, remove, isLoading, error };
1291
+ }
1292
+ function useAdminBlogCategories() {
1293
+ const { client } = useFoxPixelContext();
1294
+ const [data, setData] = useState11(null);
1295
+ const [isLoading, setIsLoading] = useState11(true);
1296
+ const [error, setError] = useState11(null);
1297
+ const fetchCategories = useCallback5(async () => {
1298
+ try {
1299
+ setIsLoading(true);
1300
+ setError(null);
1301
+ const result = await client.get("/api/blog/categories");
1302
+ setData(Array.isArray(result) ? result : []);
1303
+ } catch (err) {
1304
+ setError(err);
1305
+ } finally {
1306
+ setIsLoading(false);
1307
+ }
1308
+ }, [client]);
1309
+ useEffect9(() => {
1310
+ fetchCategories();
1311
+ }, [fetchCategories]);
1312
+ const create = async (payload) => {
1313
+ try {
1314
+ const result = await client.post("/api/blog/categories", payload);
1315
+ await fetchCategories();
1316
+ return result;
1317
+ } catch (err) {
1318
+ setError(err);
1319
+ return null;
1320
+ }
1321
+ };
1322
+ const update = async (id, payload) => {
1323
+ try {
1324
+ const result = await client.put(`/api/blog/categories/${id}`, payload);
1325
+ await fetchCategories();
1326
+ return result;
1327
+ } catch (err) {
1328
+ setError(err);
1329
+ return null;
1330
+ }
1331
+ };
1332
+ const remove = async (id) => {
1333
+ try {
1334
+ await client.delete(`/api/blog/categories/${id}`);
1335
+ await fetchCategories();
1336
+ return true;
1337
+ } catch (err) {
1338
+ setError(err);
1339
+ return false;
1340
+ }
1341
+ };
1342
+ return { data, isLoading, error, refetch: fetchCategories, create, update, remove };
1343
+ }
1344
+ function useAdminBlogTags() {
1345
+ const { client } = useFoxPixelContext();
1346
+ const [data, setData] = useState11(null);
1347
+ const [isLoading, setIsLoading] = useState11(true);
1348
+ const [error, setError] = useState11(null);
1349
+ const fetchTags = useCallback5(async () => {
1350
+ try {
1351
+ setIsLoading(true);
1352
+ setError(null);
1353
+ const result = await client.get("/api/blog/tags");
1354
+ setData(Array.isArray(result) ? result : []);
1355
+ } catch (err) {
1356
+ setError(err);
1357
+ } finally {
1358
+ setIsLoading(false);
1359
+ }
1360
+ }, [client]);
1361
+ useEffect9(() => {
1362
+ fetchTags();
1363
+ }, [fetchTags]);
1364
+ const create = async (payload) => {
1365
+ try {
1366
+ const result = await client.post("/api/blog/tags", payload);
1367
+ await fetchTags();
1368
+ return result;
1369
+ } catch (err) {
1370
+ setError(err);
1371
+ return null;
1372
+ }
1373
+ };
1374
+ const update = async (id, payload) => {
1375
+ try {
1376
+ const result = await client.put(`/api/blog/tags/${id}`, payload);
1377
+ await fetchTags();
1378
+ return result;
1379
+ } catch (err) {
1380
+ setError(err);
1381
+ return null;
1382
+ }
1383
+ };
1384
+ const remove = async (id) => {
1385
+ try {
1386
+ await client.delete(`/api/blog/tags/${id}`);
1387
+ await fetchTags();
1388
+ return true;
1389
+ } catch (err) {
1390
+ setError(err);
1391
+ return false;
1392
+ }
1393
+ };
1394
+ return { data, isLoading, error, refetch: fetchTags, create, update, remove };
1395
+ }
1396
+ function useAdminBlogComments(options = {}) {
1397
+ const { client } = useFoxPixelContext();
1398
+ const [data, setData] = useState11(null);
1399
+ const [isLoading, setIsLoading] = useState11(true);
1400
+ const [error, setError] = useState11(null);
1401
+ const { status, postId, page = 0, size = 20 } = options;
1402
+ const fetchComments = useCallback5(async () => {
1403
+ try {
1404
+ setIsLoading(true);
1405
+ setError(null);
1406
+ const params = new URLSearchParams();
1407
+ params.append("page", String(page));
1408
+ params.append("size", String(size));
1409
+ if (status) params.append("status", status);
1410
+ if (postId) params.append("postId", postId);
1411
+ const result = await client.get(`/api/blog/comments?${params.toString()}`);
1412
+ setData(result);
1413
+ } catch (err) {
1414
+ setError(err);
1415
+ } finally {
1416
+ setIsLoading(false);
1417
+ }
1418
+ }, [client, status, postId, page, size]);
1419
+ useEffect9(() => {
1420
+ fetchComments();
1421
+ }, [fetchComments]);
1422
+ const updateStatus = async (id, newStatus) => {
1423
+ try {
1424
+ await client.put(`/api/blog/comments/${id}/status`, { status: newStatus });
1425
+ await fetchComments();
1426
+ return true;
1427
+ } catch (err) {
1428
+ setError(err);
1429
+ return false;
1430
+ }
1431
+ };
1432
+ const remove = async (id) => {
1433
+ try {
1434
+ await client.delete(`/api/blog/comments/${id}`);
1435
+ await fetchComments();
1436
+ return true;
1437
+ } catch (err) {
1438
+ setError(err);
1439
+ return false;
1440
+ }
1441
+ };
1442
+ return { data, isLoading, error, refetch: fetchComments, updateStatus, remove };
1443
+ }
1444
+ function useAdminNewsletterSubscribers(options = {}) {
1445
+ const { client } = useFoxPixelContext();
1446
+ const [data, setData] = useState11(null);
1447
+ const [isLoading, setIsLoading] = useState11(true);
1448
+ const [error, setError] = useState11(null);
1449
+ const { status, page = 0, size = 20 } = options;
1450
+ const fetchSubscribers = useCallback5(async () => {
1451
+ try {
1452
+ setIsLoading(true);
1453
+ setError(null);
1454
+ const params = new URLSearchParams();
1455
+ params.append("page", String(page));
1456
+ params.append("size", String(size));
1457
+ if (status) params.append("status", status);
1458
+ const result = await client.get(`/api/blog/newsletter/subscribers?${params.toString()}`);
1459
+ setData(result);
1460
+ } catch (err) {
1461
+ setError(err);
1462
+ } finally {
1463
+ setIsLoading(false);
1464
+ }
1465
+ }, [client, status, page, size]);
1466
+ useEffect9(() => {
1467
+ fetchSubscribers();
1468
+ }, [fetchSubscribers]);
1469
+ const remove = async (id) => {
1470
+ try {
1471
+ await client.delete(`/api/blog/newsletter/subscribers/${id}`);
1472
+ await fetchSubscribers();
1473
+ return true;
1474
+ } catch (err) {
1475
+ setError(err);
1476
+ return false;
1477
+ }
1478
+ };
1479
+ return { data, isLoading, error, refetch: fetchSubscribers, remove };
1480
+ }
1481
+ function useAdminNewsletterStats() {
1482
+ const { client } = useFoxPixelContext();
1483
+ const [data, setData] = useState11(null);
1484
+ const [isLoading, setIsLoading] = useState11(true);
1485
+ const [error, setError] = useState11(null);
1486
+ const fetchStats = useCallback5(async () => {
1487
+ try {
1488
+ setIsLoading(true);
1489
+ setError(null);
1490
+ const result = await client.get("/api/blog/newsletter/stats");
1491
+ setData(result);
1492
+ } catch (err) {
1493
+ setError(err);
1494
+ } finally {
1495
+ setIsLoading(false);
1496
+ }
1497
+ }, [client]);
1498
+ useEffect9(() => {
1499
+ fetchStats();
1500
+ }, [fetchStats]);
1501
+ return { data, isLoading, error, refetch: fetchStats };
1502
+ }
1503
+ function useAdminBlogSettings() {
1504
+ const { client } = useFoxPixelContext();
1505
+ const [data, setData] = useState11(null);
1506
+ const [isLoading, setIsLoading] = useState11(true);
1507
+ const [error, setError] = useState11(null);
1508
+ const fetchSettings = useCallback5(async () => {
1509
+ try {
1510
+ setIsLoading(true);
1511
+ setError(null);
1512
+ const result = await client.get("/api/blog/settings");
1513
+ setData(result);
1514
+ } catch (err) {
1515
+ setError(err);
1516
+ } finally {
1517
+ setIsLoading(false);
1518
+ }
1519
+ }, [client]);
1520
+ useEffect9(() => {
1521
+ fetchSettings();
1522
+ }, [fetchSettings]);
1523
+ const update = async (settings) => {
1524
+ try {
1525
+ const result = await client.put("/api/blog/settings", settings);
1526
+ setData(result);
1527
+ return result;
1528
+ } catch (err) {
1529
+ setError(err);
1530
+ return null;
1531
+ }
1532
+ };
1533
+ return { data, isLoading, error, refetch: fetchSettings, update };
1534
+ }
1535
+ function useAdminBlogAnalytics() {
1536
+ const { client } = useFoxPixelContext();
1537
+ const [data, setData] = useState11(null);
1538
+ const [isLoading, setIsLoading] = useState11(true);
1539
+ const [error, setError] = useState11(null);
1540
+ const fetchAnalytics = useCallback5(async () => {
1541
+ try {
1542
+ setIsLoading(true);
1543
+ setError(null);
1544
+ const result = await client.get("/api/blog/analytics/summary");
1545
+ setData(result);
1546
+ } catch (err) {
1547
+ setError(err);
1548
+ } finally {
1549
+ setIsLoading(false);
1550
+ }
1551
+ }, [client]);
1552
+ useEffect9(() => {
1553
+ fetchAnalytics();
1554
+ }, [fetchAnalytics]);
1555
+ return { data, isLoading, error, refetch: fetchAnalytics };
1556
+ }
1557
+
1558
+ // src/blog/utils.ts
1559
+ function getBlogPostSchemaLd(post, options) {
1560
+ const { siteUrl, publisherName, publisherLogoUrl } = options;
1561
+ const postUrl = `${siteUrl.replace(/\/$/, "")}/blog/${post.slug}`;
1562
+ const schema = {
1563
+ "@context": "https://schema.org",
1564
+ "@type": "BlogPosting",
1565
+ headline: post.title,
1566
+ description: post.metaDescription || post.excerpt || void 0,
1567
+ image: post.coverImageUrl || void 0,
1568
+ datePublished: post.publishedAt || void 0,
1569
+ dateModified: post.updatedAt,
1570
+ mainEntityOfPage: {
1571
+ "@type": "WebPage",
1572
+ "@id": postUrl
1573
+ }
1574
+ };
1575
+ if (publisherName || publisherLogoUrl) {
1576
+ schema.publisher = {
1577
+ "@type": "Organization",
1578
+ ...publisherName && { name: publisherName },
1579
+ ...publisherLogoUrl && {
1580
+ logo: {
1581
+ "@type": "ImageObject",
1582
+ url: publisherLogoUrl
1583
+ }
1584
+ }
1585
+ };
1586
+ }
1587
+ return schema;
1588
+ }
435
1589
  export {
436
1590
  AuthProvider,
1591
+ Editable,
1592
+ EditableHTML,
1593
+ EditableImage,
437
1594
  FoxPixelHttpClient,
438
1595
  FoxPixelProvider,
439
1596
  GuestOnlyRoute,
440
1597
  ProtectedRoute,
1598
+ SITE_CONTENT_QUERY_KEY,
1599
+ getBlogPostSchemaLd,
1600
+ useAdminBlogAnalytics,
1601
+ useAdminBlogCategories,
1602
+ useAdminBlogComments,
1603
+ useAdminBlogPost,
1604
+ useAdminBlogPostMutations,
1605
+ useAdminBlogPosts,
1606
+ useAdminBlogSettings,
1607
+ useAdminBlogTags,
1608
+ useAdminNewsletterStats,
1609
+ useAdminNewsletterSubscribers,
441
1610
  useAuth,
1611
+ useBlogCategories,
1612
+ useBlogCommentSubmit,
1613
+ useBlogComments,
1614
+ useBlogFeaturedPosts,
1615
+ useBlogPost,
1616
+ useBlogPosts,
1617
+ useBlogTags,
442
1618
  useContactCapture,
1619
+ useEditMode,
1620
+ useEditModeMessaging,
443
1621
  useFoxPixelContext,
444
1622
  useLeadCapture,
1623
+ useNewsletterSubscribe,
1624
+ useNewsletterUnsubscribe,
1625
+ useSendEditRequest,
445
1626
  useServices,
1627
+ useSiteContent,
1628
+ useSiteContentQuery,
1629
+ useSiteContentSection,
1630
+ useSiteContents,
446
1631
  withAuth
447
1632
  };
448
1633
  //# sourceMappingURL=index.mjs.map