@rxdrag/website-lib-core 0.0.4 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/index.ts +1 -0
  2. package/package.json +12 -13
  3. package/src/entify/Entify.ts +365 -0
  4. package/{dist/entify/index.d.ts → src/entify/index.ts} +4 -4
  5. package/src/entify/lib/createEntifyClient.ts +23 -0
  6. package/{dist/entify/lib/index.d.ts → src/entify/lib/index.ts} +29 -29
  7. package/src/entify/lib/langFields.ts +12 -0
  8. package/src/entify/lib/newAvatarQueryOptions.ts +5 -0
  9. package/src/entify/lib/newOgImageQueryOptions.ts +6 -0
  10. package/src/entify/lib/newPageMetaOptions.ts +20 -0
  11. package/src/entify/lib/newQueryPostOptions.ts +41 -0
  12. package/src/entify/lib/newQueryProductOptions.ts +90 -0
  13. package/src/entify/lib/newQueryProductsMediaOptions.ts +26 -0
  14. package/src/entify/lib/queryAllProducts.ts +27 -0
  15. package/src/entify/lib/queryEntityList.ts +44 -0
  16. package/src/entify/lib/queryFeaturedProducts.ts +47 -0
  17. package/src/entify/lib/queryLangs.ts +47 -0
  18. package/src/entify/lib/queryLatestPosts.ts +65 -0
  19. package/src/entify/lib/queryOneEntity.ts +67 -0
  20. package/src/entify/lib/queryOnePostById.ts +21 -0
  21. package/src/entify/lib/queryOnePostBySlug.ts +21 -0
  22. package/src/entify/lib/queryOnePostCategoryBySlug.ts +30 -0
  23. package/src/entify/lib/queryOneProductById.ts +20 -0
  24. package/src/entify/lib/queryOneProductBySlug.ts +21 -0
  25. package/src/entify/lib/queryOneProductCategoryBySlug.ts +30 -0
  26. package/src/entify/lib/queryOneTheme.ts +76 -0
  27. package/src/entify/lib/queryOneUser.ts +38 -0
  28. package/src/entify/lib/queryPostCategories.ts +48 -0
  29. package/src/entify/lib/queryPostSlugs.ts +32 -0
  30. package/src/entify/lib/queryPosts.ts +92 -0
  31. package/src/entify/lib/queryProductCategories.ts +44 -0
  32. package/src/entify/lib/queryProducts.ts +69 -0
  33. package/src/entify/lib/queryProductsInMenu.ts +31 -0
  34. package/src/entify/lib/queryUserIds.ts +24 -0
  35. package/src/entify/lib/queryUserPosts.ts +74 -0
  36. package/src/entify/lib/queryWebSiteSettings.ts +29 -0
  37. package/src/entify/lib/searchProducts.ts +70 -0
  38. package/src/entify/lib/sendEmail.ts +8 -0
  39. package/src/entify/lib/toQueryOptions.ts +20 -0
  40. package/src/entify/lib/upsertEntity.ts +9 -0
  41. package/src/entify/types/index.ts +2 -0
  42. package/src/entify/types/utils.ts +4 -0
  43. package/src/entify/types/variables.ts +7 -0
  44. package/src/entify/view-model/funcs.ts +271 -0
  45. package/src/entify/view-model/index.ts +2 -0
  46. package/src/entify/view-model/models.ts +143 -0
  47. package/{dist/index.d.ts → src/index.ts} +5 -5
  48. package/src/motion/consts.ts +598 -0
  49. package/{dist/motion/index.d.ts → src/motion/index.ts} +2 -2
  50. package/src/motion/types.ts +46 -0
  51. package/src/react/components/EnquiryForm/Input.tsx +52 -0
  52. package/src/react/components/EnquiryForm/Submit.tsx +30 -0
  53. package/src/react/components/EnquiryForm/Textarea.tsx +51 -0
  54. package/src/react/components/EnquiryForm/index.tsx +334 -0
  55. package/src/react/components/GoogleConsent/CookieItemPanel.tsx +81 -0
  56. package/src/react/components/GoogleConsent/CumtomizedModal.tsx +149 -0
  57. package/src/react/components/GoogleConsent/GoogleConsent.tsx +101 -0
  58. package/src/react/components/GoogleConsent/README.md +1 -0
  59. package/src/react/components/GoogleConsent/gtags.ts +68 -0
  60. package/src/react/components/GoogleConsent/index.ts +3 -0
  61. package/src/react/components/GoogleConsent/types.ts +18 -0
  62. package/src/react/components/GoogleConsent//345/217/202/350/200/203.md +4 -0
  63. package/src/react/components/Medias/index.tsx +347 -0
  64. package/src/react/components/ProductCard/ProductCard.tsx +23 -0
  65. package/src/react/components/ProductCard/ProductCardPreview.tsx +12 -0
  66. package/src/react/components/ProductCard/ProductCta/index.tsx +41 -0
  67. package/src/react/components/ProductCard/ProductCta/style.css +4 -0
  68. package/src/react/components/ProductCard/ProductDescription/index.tsx +13 -0
  69. package/src/react/components/ProductCard/ProductDescription/style.css +6 -0
  70. package/src/react/components/ProductCard/ProductMedia/index.tsx +34 -0
  71. package/src/react/components/ProductCard/ProductMedia/style.css +6 -0
  72. package/src/react/components/ProductCard/ProductTitle/index.tsx +7 -0
  73. package/src/react/components/ProductCard/ProductTitle/style.css +4 -0
  74. package/src/react/components/ProductCard/ProductView.tsx +35 -0
  75. package/{dist/react/components/ProductCard/index.d.ts → src/react/components/ProductCard/index.ts} +6 -6
  76. package/src/react/components/ProductCard/useQueryProduct.ts +32 -0
  77. package/src/react/components/RichTextOutline/index.tsx +76 -0
  78. package/src/react/components/RichTextOutline/useAcitviedHeading.ts +54 -0
  79. package/src/react/components/RichTextOutline/useAnchorScroll.ts +24 -0
  80. package/src/react/components/Scroller.tsx +7 -0
  81. package/src/react/components/SearchInput.tsx +34 -0
  82. package/src/react/components/Share/index.tsx +69 -0
  83. package/src/react/components/Share/socials.tsx +79 -0
  84. package/src/react/components/Share//350/265/204/346/226/231.md +7 -0
  85. package/src/react/components/ToTop/index.tsx +33 -0
  86. package/src/react/components/ToTop.tsx +33 -0
  87. package/{dist/react/components/index.d.ts → src/react/components/index.ts} +8 -8
  88. package/src/react/hooks/index.ts +1 -0
  89. package/src/react/hooks/useScroll.ts +23 -0
  90. package/{dist/react/index.d.ts → src/react/index.ts} +2 -2
  91. package/src/robots.ts +4 -0
  92. package/src/scripts/actions.ts +304 -0
  93. package/src/scripts/consts.ts +32 -0
  94. package/src/scripts/events.ts +33 -0
  95. package/{dist/scripts/index.d.ts → src/scripts/index.ts} +3 -3
  96. package/dist/entify/Entify.d.ts +0 -138
  97. package/dist/entify/lib/createEntifyClient.d.ts +0 -3
  98. package/dist/entify/lib/langFields.d.ts +0 -2
  99. package/dist/entify/lib/newAvatarQueryOptions.d.ts +0 -2
  100. package/dist/entify/lib/newOgImageQueryOptions.d.ts +0 -2
  101. package/dist/entify/lib/newPageMetaOptions.d.ts +0 -2
  102. package/dist/entify/lib/newQueryPostOptions.d.ts +0 -3
  103. package/dist/entify/lib/newQueryProductOptions.d.ts +0 -3
  104. package/dist/entify/lib/newQueryProductsMediaOptions.d.ts +0 -3
  105. package/dist/entify/lib/queryAllProducts.d.ts +0 -4
  106. package/dist/entify/lib/queryEntityList.d.ts +0 -3
  107. package/dist/entify/lib/queryFeaturedProducts.d.ts +0 -4
  108. package/dist/entify/lib/queryLangs.d.ts +0 -4
  109. package/dist/entify/lib/queryLatestPosts.d.ts +0 -4
  110. package/dist/entify/lib/queryOneEntity.d.ts +0 -5
  111. package/dist/entify/lib/queryOnePostById.d.ts +0 -3
  112. package/dist/entify/lib/queryOnePostBySlug.d.ts +0 -3
  113. package/dist/entify/lib/queryOnePostCategoryBySlug.d.ts +0 -3
  114. package/dist/entify/lib/queryOneProductById.d.ts +0 -3
  115. package/dist/entify/lib/queryOneProductBySlug.d.ts +0 -3
  116. package/dist/entify/lib/queryOneProductCategoryBySlug.d.ts +0 -3
  117. package/dist/entify/lib/queryOneTheme.d.ts +0 -3
  118. package/dist/entify/lib/queryOneUser.d.ts +0 -3
  119. package/dist/entify/lib/queryPostCategories.d.ts +0 -4
  120. package/dist/entify/lib/queryPostSlugs.d.ts +0 -4
  121. package/dist/entify/lib/queryPosts.d.ts +0 -10
  122. package/dist/entify/lib/queryProductCategories.d.ts +0 -4
  123. package/dist/entify/lib/queryProducts.d.ts +0 -6
  124. package/dist/entify/lib/queryProductsInMenu.d.ts +0 -2
  125. package/dist/entify/lib/queryUserIds.d.ts +0 -4
  126. package/dist/entify/lib/queryUserPosts.d.ts +0 -9
  127. package/dist/entify/lib/queryWebSiteSettings.d.ts +0 -3
  128. package/dist/entify/lib/searchProducts.d.ts +0 -4
  129. package/dist/entify/lib/sendEmail.d.ts +0 -3
  130. package/dist/entify/lib/toQueryOptions.d.ts +0 -3
  131. package/dist/entify/lib/upsertEntity.d.ts +0 -2
  132. package/dist/entify/types/index.d.ts +0 -2
  133. package/dist/entify/types/utils.d.ts +0 -4
  134. package/dist/entify/types/variables.d.ts +0 -7
  135. package/dist/entify/view-model/funcs.d.ts +0 -20
  136. package/dist/entify/view-model/index.d.ts +0 -2
  137. package/dist/entify/view-model/models.d.ts +0 -119
  138. package/dist/index.mjs +0 -40514
  139. package/dist/index.mjs.map +0 -1
  140. package/dist/motion/consts.d.ts +0 -77
  141. package/dist/motion/types.d.ts +0 -26
  142. package/dist/react/components/EnquiryForm/Input.d.ts +0 -15
  143. package/dist/react/components/EnquiryForm/Submit.d.ts +0 -8
  144. package/dist/react/components/EnquiryForm/Textarea.d.ts +0 -13
  145. package/dist/react/components/EnquiryForm/index.d.ts +0 -22
  146. package/dist/react/components/Medias/index.d.ts +0 -8
  147. package/dist/react/components/ProductCard/ProductCard.d.ts +0 -15
  148. package/dist/react/components/ProductCard/ProductCardPreview.d.ts +0 -2
  149. package/dist/react/components/ProductCard/ProductCta/index.d.ts +0 -5
  150. package/dist/react/components/ProductCard/ProductDescription/index.d.ts +0 -2
  151. package/dist/react/components/ProductCard/ProductMedia/index.d.ts +0 -7
  152. package/dist/react/components/ProductCard/ProductTitle/index.d.ts +0 -2
  153. package/dist/react/components/ProductCard/ProductView.d.ts +0 -5
  154. package/dist/react/components/ProductCard/useQueryProduct.d.ts +0 -2
  155. package/dist/react/components/RichTextOutline/index.d.ts +0 -8
  156. package/dist/react/components/RichTextOutline/useAcitviedHeading.d.ts +0 -1
  157. package/dist/react/components/Scroller.d.ts +0 -3
  158. package/dist/react/components/SearchInput.d.ts +0 -2
  159. package/dist/react/components/Share/index.d.ts +0 -6
  160. package/dist/react/components/Share/socials.d.ts +0 -10
  161. package/dist/react/components/ToTop.d.ts +0 -5
  162. package/dist/react/hooks/index.d.ts +0 -1
  163. package/dist/react/hooks/useScroll.d.ts +0 -2
  164. package/dist/robots.d.ts +0 -2
  165. package/dist/scripts/actions.d.ts +0 -85
  166. package/dist/scripts/consts.d.ts +0 -21
  167. package/dist/scripts/events.d.ts +0 -11
  168. package/dist/style.css +0 -98
@@ -0,0 +1,101 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { CumtomizedModal } from "./CumtomizedModal";
3
+ import Cookies from "universal-cookie"
4
+ import { ConsentSettings } from "./types";
5
+ import { Container } from "../Container";
6
+ import { gtag } from "./gtags";
7
+
8
+ const COOKIE_NAME = "gdpr-consent";
9
+
10
+ const DEINED_ALL: ConsentSettings = {
11
+ strictlyNecessary: true,
12
+ analytics: false,
13
+ functionality: false,
14
+ advertisement: false,
15
+ }
16
+
17
+ export function XGoogleConsent(props: {
18
+ domain?: string
19
+ }) {
20
+ const { domain } = props;
21
+
22
+ const [decisionMade, setDecisionMade] = useState(true) // start with true to avoid flashing
23
+ const cookies = useMemo(() => new Cookies(), []);
24
+
25
+ useEffect(() => {
26
+ const consent = cookies.get(COOKIE_NAME);
27
+ if (consent !== undefined) {
28
+ setDecisionMade(true)
29
+ sendConsent(consent)
30
+ } else {
31
+ setDecisionMade(false)
32
+ // 默认设置
33
+ gtag.consent(DEINED_ALL);
34
+ }
35
+ }, [cookies, setDecisionMade])
36
+
37
+ const handleSavePreferences = (consent: ConsentSettings) => {
38
+ saveDecision(consent)
39
+ };
40
+
41
+ const handleDeclineAll = () => {
42
+ saveDecision(DEINED_ALL)
43
+ };
44
+
45
+ const saveDecision = (consent: ConsentSettings) => {
46
+ sendConsent(consent);
47
+
48
+ cookies.set(COOKIE_NAME, consent, {
49
+ expires: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
50
+ path: "/",
51
+ domain: domain
52
+ })
53
+
54
+ setDecisionMade(true)
55
+ }
56
+
57
+ const sendConsent = (consent: ConsentSettings) => {
58
+ gtag.consent(consent);
59
+ }
60
+
61
+ const handleAcceptAll = () => {
62
+ saveDecision({
63
+ advertisement: true,
64
+ analytics: true,
65
+ functionality: true,
66
+ strictlyNecessary: true
67
+ })
68
+ };
69
+
70
+ if (decisionMade) {
71
+ return null;
72
+ }
73
+
74
+ return (
75
+ <div className="fixed left-0 bottom-0 w-full shadow-lg bg-gray-600">
76
+ <Container>
77
+ <div className="text-white p-6 w-full">
78
+ <div className="flex flex-1 items-center gap-8 justify-between">
79
+ <div className="text-sm">
80
+ <h2 className="text-md font-bold pb-2">We Value Your Privacy</h2>
81
+ We use cookies for site improvement, personalization, and ads. Accepting enhances your experience, but you can manage preferences or decline non-essential cookies. See our <a className="text-blue-500" target="_blank" href="/privacy-policy">Cookie Policy</a> for details.
82
+ </div>
83
+ <div className="grid grid-cols-2 w-[300px] gap-2 text-sm">
84
+ <button onClick={handleAcceptAll} className="bg-green-400 hover:bg-green-500 text-white py-2 px-2 rounded">
85
+ Accept All
86
+ </button>
87
+ <button onClick={handleDeclineAll} className="text-white py-2 px-2 rounded bg-gray-700 hover:bg-gray-800">
88
+ Decline All
89
+ </button>
90
+ <CumtomizedModal
91
+ onAcceptAll={handleAcceptAll}
92
+ onRejectAll={handleDeclineAll}
93
+ onSavePreferences={handleSavePreferences}
94
+ />
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </Container>
99
+ </div>
100
+ );
101
+ }
@@ -0,0 +1 @@
1
+ 统一配置,根据语言设置,自动切换语言,不需要作为页面组件
@@ -0,0 +1,68 @@
1
+ import { ConsentSettings } from "./types";
2
+
3
+ declare global {
4
+ interface Window {
5
+ gtag: (...args: unknown[]) => void;
6
+ }
7
+ }
8
+
9
+ export const consent = (consent: ConsentSettings) => {
10
+ if (!window.gtag) {
11
+ console.warn(
12
+ "window.gtag is not defined. This could mean your google analytics script has not loaded on the page yet.",
13
+ );
14
+ return;
15
+ }
16
+
17
+ window.gtag("consent", "update", {
18
+ ad_storage: consent.advertisement ? "granted" : "denied",
19
+ analytics_storage: consent.analytics ? "granted" : "denied",
20
+ ad_user_data: consent.advertisement ? "granted" : "denied",
21
+ ad_personalization: consent.advertisement ? "granted" : "denied",
22
+ });
23
+ };
24
+
25
+ /**
26
+ * @example
27
+ * https://developers.google.com/analytics/devguides/collection/gtagjs/pages
28
+ */
29
+ export const pageview = (url: string, trackingId: string) => {
30
+ if (!window.gtag) {
31
+ console.warn(
32
+ "window.gtag is not defined. This could mean your google analytics script has not loaded on the page yet.",
33
+ );
34
+ return;
35
+ }
36
+ window.gtag("config", trackingId, {
37
+ page_path: url,
38
+ });
39
+ };
40
+
41
+ /**
42
+ * @example
43
+ * https://developers.google.com/analytics/devguides/collection/gtagjs/events
44
+ */
45
+ export const event = ({
46
+ action,
47
+ category,
48
+ label,
49
+ value,
50
+ }: Record<string, string>) => {
51
+ if (!window.gtag) {
52
+ console.warn(
53
+ "window.gtag is not defined. This could mean your google analytics script has not loaded on the page yet.",
54
+ );
55
+ return;
56
+ }
57
+ window.gtag("event", action, {
58
+ event_category: category,
59
+ event_label: label,
60
+ value: value,
61
+ });
62
+ };
63
+
64
+ export const gtag = {
65
+ consent,
66
+ pageview,
67
+ event
68
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export * from "./GoogleConsent";
3
+ export * from "./gtags";
@@ -0,0 +1,18 @@
1
+ export type ConsentSettings = {
2
+ strictlyNecessary: boolean;
3
+ analytics: boolean;
4
+ functionality: boolean;
5
+ advertisement: boolean;
6
+ };
7
+
8
+
9
+ export type CookieItem = {
10
+ title: string;
11
+ content: string;
12
+ checkbox: {
13
+ valueName: string;
14
+ label?: string;
15
+ alwaysActive?: boolean;
16
+ }
17
+ }
18
+
@@ -0,0 +1,4 @@
1
+ https://baijiahao.baidu.com/s?id=1794747324846977848&wfr=spider&for=pc
2
+ https://github.com/fordooo/use-consent-mode-cookie-banner/tree/main
3
+ https://github.com/orestbida/cookieconsent
4
+ https://maxket.com/google-consent-mode-v2/
@@ -0,0 +1,347 @@
1
+ import clsx from "clsx";
2
+ import { forwardRef, useEffect, useState, useCallback } from "react";
3
+ import { TMedias } from "../../../entify";
4
+
5
+ export type MediasProps = {
6
+ value?: TMedias;
7
+ className?: string;
8
+ children?: React.ReactNode;
9
+ // Aspect ratio, format is `aspect-[width/height]`
10
+ aspect?: string;
11
+ };
12
+
13
+ export const Medias = forwardRef<HTMLDivElement, MediasProps>(
14
+ (props, ref) => {
15
+ const {
16
+ value,
17
+ className,
18
+ children,
19
+ aspect = "aspect-[5/4]",
20
+ ...rest
21
+ } = props;
22
+ const [selectedId, setSelectedId] = useState<string | undefined | null>(
23
+ value?.externalVideoUrl ? "video" : value?.medias?.[0]?.id || ""
24
+ );
25
+ const [videoUrl, setVideoUrl] = useState<string>("");
26
+ const [thumbnailUrl, setThumbnailUrl] = useState<string>("");
27
+
28
+ useEffect(() => {
29
+ setSelectedId(
30
+ value?.externalVideoUrl ? "video" : value?.medias?.[0]?.id || ""
31
+ );
32
+ }, [value]);
33
+
34
+ useEffect(() => {
35
+ if (value?.externalVideoUrl) {
36
+ const baseUrl = value.externalVideoUrl.replace(
37
+ "https://youtu.be/",
38
+ "https://www.youtube.com/embed/"
39
+ );
40
+ const separator = baseUrl.includes("?") ? "&" : "?";
41
+ setVideoUrl(
42
+ `${baseUrl}${separator}autoplay=1&muted=1&modestbranding=1&rel=0&controls=1&playsinline=1&enablejsapi=1&origin=${encodeURIComponent(
43
+ window.location.origin
44
+ )}`
45
+ );
46
+ }
47
+ }, [value?.externalVideoUrl]);
48
+
49
+ useEffect(() => {
50
+ if (value?.externalVideoUrl) {
51
+ const videoId = value.externalVideoUrl.includes("youtu.be/")
52
+ ? value.externalVideoUrl.split("youtu.be/")[1].split("?")[0]
53
+ : value.externalVideoUrl.split("v=")[1]?.split("&")[0];
54
+
55
+ if (videoId) {
56
+ setThumbnailUrl(
57
+ `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`
58
+ );
59
+ }
60
+ }
61
+ }, [value?.externalVideoUrl]);
62
+
63
+ const selectedIndex = value?.externalVideoUrl
64
+ ? selectedId === "video"
65
+ ? 0
66
+ : (value?.medias?.findIndex((media) => media.id === selectedId) || 0) +
67
+ 1
68
+ : value?.medias?.findIndex((media) => media.id === selectedId) || 0;
69
+
70
+ const totalItems =
71
+ (value?.externalVideoUrl ? 1 : 0) + (value?.medias?.length || 0);
72
+
73
+ // Calculate visible thumbnails (show 6 items)
74
+ const visibleCount = 6;
75
+ const halfVisible = Math.floor(visibleCount / 2);
76
+ const startIndex = Math.max(
77
+ 0,
78
+ Math.min(selectedIndex - halfVisible, totalItems - visibleCount)
79
+ );
80
+ const endIndex = Math.min(startIndex + visibleCount, totalItems);
81
+
82
+ const handlePrevious = useCallback(() => {
83
+ if (selectedIndex > 0) {
84
+ if (value?.externalVideoUrl) {
85
+ if (selectedIndex === 1) {
86
+ setSelectedId("video");
87
+ } else {
88
+ const prevIndex = selectedIndex - 2;
89
+ setSelectedId(value.medias?.[prevIndex]?.id || "");
90
+ }
91
+ } else {
92
+ const prevIndex = selectedIndex - 1;
93
+ setSelectedId(value?.medias?.[prevIndex]?.id || "");
94
+ }
95
+ }
96
+ }, [selectedIndex, value]);
97
+
98
+ const handleNext = useCallback(() => {
99
+ if (selectedIndex < totalItems - 1) {
100
+ if (value?.externalVideoUrl) {
101
+ if (selectedId === "video") {
102
+ setSelectedId(value.medias?.[0]?.id || "");
103
+ } else {
104
+ const currentMediaIndex =
105
+ value.medias?.findIndex((media) => media.id === selectedId) || 0;
106
+ const nextIndex = currentMediaIndex + 1;
107
+ setSelectedId(value.medias?.[nextIndex]?.id || "");
108
+ }
109
+ } else {
110
+ const currentMediaIndex =
111
+ value?.medias?.findIndex((media) => media.id === selectedId) || 0;
112
+ const nextIndex = currentMediaIndex + 1;
113
+ setSelectedId(value?.medias?.[nextIndex]?.id || "");
114
+ }
115
+ }
116
+ }, [selectedIndex, totalItems, value, selectedId]);
117
+
118
+ const handleKeyDown = useCallback(
119
+ (e: KeyboardEvent) => {
120
+ if (e.key === "ArrowLeft") {
121
+ handlePrevious();
122
+ } else if (e.key === "ArrowRight") {
123
+ handleNext();
124
+ }
125
+ },
126
+ [handleNext, handlePrevious]
127
+ );
128
+
129
+ useEffect(() => {
130
+ window.addEventListener("keydown", handleKeyDown);
131
+ return () => window.removeEventListener("keydown", handleKeyDown);
132
+ }, [handleKeyDown]);
133
+
134
+ const canPrevious = selectedIndex > 0;
135
+ const canNext = selectedIndex < totalItems - 1;
136
+
137
+ return (
138
+ <div ref={ref} className={clsx("flex flex-col", className)} {...rest}>
139
+ {children}
140
+ {/* Main display area */}
141
+ <div className={clsx("relative group")}>
142
+ {canPrevious && (
143
+ <button
144
+ onClick={handlePrevious}
145
+ className="absolute left-4 top-1/2 transform -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
146
+ aria-label="Previous slide"
147
+ >
148
+ <svg
149
+ xmlns="http://www.w3.org/2000/svg"
150
+ className="h-6 w-6"
151
+ fill="none"
152
+ viewBox="0 0 24 24"
153
+ stroke="currentColor"
154
+ >
155
+ <path
156
+ strokeLinecap="round"
157
+ strokeLinejoin="round"
158
+ strokeWidth={2}
159
+ d="M15 19l-7-7 7-7"
160
+ />
161
+ </svg>
162
+ </button>
163
+ )}
164
+ {canNext && (
165
+ <button
166
+ onClick={handleNext}
167
+ className="absolute right-4 top-1/2 transform -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
168
+ aria-label="Next slide"
169
+ >
170
+ <svg
171
+ xmlns="http://www.w3.org/2000/svg"
172
+ className="h-6 w-6"
173
+ fill="none"
174
+ viewBox="0 0 24 24"
175
+ stroke="currentColor"
176
+ >
177
+ <path
178
+ strokeLinecap="round"
179
+ strokeLinejoin="round"
180
+ strokeWidth={2}
181
+ d="M9 5l7 7-7 7"
182
+ />
183
+ </svg>
184
+ </button>
185
+ )}
186
+ {value?.externalVideoUrl && selectedId === "video" && (
187
+ <div className={clsx("w-full rounded-md overflow-hidden", aspect)}>
188
+ <iframe
189
+ src={videoUrl}
190
+ className="w-full h-full"
191
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; autoplay"
192
+ referrerPolicy="strict-origin-when-cross-origin"
193
+ allowFullScreen
194
+ />
195
+ </div>
196
+ )}
197
+ {value?.medias?.map((media) => (
198
+ <div
199
+ key={media.id}
200
+ className={clsx(
201
+ "transition-opacity duration-300 overflow-hidden rounded-md",
202
+ media.id === selectedId ? "opacity-100" : "opacity-0 hidden",
203
+ aspect
204
+ )}
205
+ >
206
+ <img
207
+ src={media?.resize || media?.url}
208
+ alt={media?.alt}
209
+ className="w-full h-full object-cover object-center"
210
+ />
211
+ </div>
212
+ ))}
213
+ </div>
214
+
215
+ {/* Thumbnail navigation */}
216
+ <div className="relative mt-4">
217
+ <div className="flex items-stretch gap-2">
218
+ {totalItems > 6 && (
219
+ <button
220
+ onClick={handlePrevious}
221
+ disabled={!canPrevious}
222
+ className={clsx(
223
+ "flex items-center justify-center w-6",
224
+ "transition-colors duration-200 rounded-l-md",
225
+ !canPrevious
226
+ ? "bg-gray-100 text-gray-400 cursor-not-allowed"
227
+ : "bg-gray-200 hover:bg-gray-300 text-gray-700"
228
+ )}
229
+ aria-label="Previous"
230
+ >
231
+ <svg
232
+ xmlns="http://www.w3.org/2000/svg"
233
+ className="h-4 w-4 md:h-5 md:w-5"
234
+ fill="none"
235
+ viewBox="0 0 24 24"
236
+ stroke="currentColor"
237
+ >
238
+ <path
239
+ strokeLinecap="round"
240
+ strokeLinejoin="round"
241
+ strokeWidth={2}
242
+ d="M15 19l-7-7 7-7"
243
+ />
244
+ </svg>
245
+ </button>
246
+ )}
247
+ <div className="flex-1">
248
+ <div className="grid grid-cols-6 gap-2">
249
+ {value?.externalVideoUrl && startIndex === 0 && (
250
+ <div
251
+ className={clsx(
252
+ "relative cursor-pointer overflow-hidden rounded-sm border-2",
253
+ aspect,
254
+ selectedId === "video"
255
+ ? "border-primary-500"
256
+ : "border-transparent hover:border-primary-300"
257
+ )}
258
+ onClick={() => setSelectedId("video")}
259
+ >
260
+ <img
261
+ src={thumbnailUrl}
262
+ alt="Video thumbnail"
263
+ className="w-full h-full object-cover"
264
+ />
265
+ <div className="absolute inset-0 flex items-center justify-center bg-black/30">
266
+ <svg
267
+ xmlns="http://www.w3.org/2000/svg"
268
+ className="h-8 w-8 text-white"
269
+ viewBox="0 0 24 24"
270
+ >
271
+ <path
272
+ fill="currentColor"
273
+ fill-rule="evenodd"
274
+ d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10"
275
+ clipRule="evenodd"
276
+ opacity="0.5"
277
+ />
278
+ <path
279
+ fill="currentColor"
280
+ d="m15.414 13.059l-4.72 2.787C9.934 16.294 9 15.71 9 14.786V9.214c0-.924.934-1.507 1.694-1.059l4.72 2.787c.781.462.781 1.656 0 2.118"
281
+ />
282
+ </svg>
283
+ </div>
284
+ </div>
285
+ )}
286
+ {value?.medias?.map((media, index) => {
287
+ const adjustedIndex = value?.externalVideoUrl
288
+ ? index + 1
289
+ : index;
290
+ return adjustedIndex >= startIndex &&
291
+ adjustedIndex < endIndex ? (
292
+ <div
293
+ key={media.id}
294
+ className={clsx(
295
+ "relative cursor-pointer overflow-hidden rounded-sm border-2",
296
+ aspect,
297
+ selectedId === media.id
298
+ ? "border-primary-500"
299
+ : "border-transparent hover:border-primary-300"
300
+ )}
301
+ onClick={() => setSelectedId(media.id)}
302
+ >
303
+ <img
304
+ src={media?.resize || media?.url}
305
+ alt={media?.alt}
306
+ className="w-full h-full object-cover"
307
+ />
308
+ </div>
309
+ ) : null;
310
+ })}
311
+ </div>
312
+ </div>
313
+ {totalItems > 6 && (
314
+ <button
315
+ onClick={handleNext}
316
+ disabled={!canNext}
317
+ className={clsx(
318
+ "flex items-center justify-center w-6",
319
+ "transition-colors duration-200 rounded-r-md",
320
+ !canNext
321
+ ? "bg-gray-100 text-gray-400 cursor-not-allowed"
322
+ : "bg-gray-200 hover:bg-gray-300 text-gray-700"
323
+ )}
324
+ aria-label="Next"
325
+ >
326
+ <svg
327
+ xmlns="http://www.w3.org/2000/svg"
328
+ className="h-4 w-4 md:h-5 md:w-5"
329
+ fill="none"
330
+ viewBox="0 0 24 24"
331
+ stroke="currentColor"
332
+ >
333
+ <path
334
+ strokeLinecap="round"
335
+ strokeLinejoin="round"
336
+ strokeWidth={2}
337
+ d="M9 5l7 7-7 7"
338
+ />
339
+ </svg>
340
+ </button>
341
+ )}
342
+ </div>
343
+ </div>
344
+ </div>
345
+ );
346
+ }
347
+ );
@@ -0,0 +1,23 @@
1
+ import { CSSProperties } from "react";
2
+ import { TSlateResizable } from "@rxdrag/slate-preview";
3
+ import { ProductView } from "./ProductView";
4
+ import { Product } from "@rxdrag/rxcms-models";
5
+
6
+ export type TSlateProduct = TSlateResizable & {
7
+ product?: Product;
8
+ title?: string;
9
+ description?: string;
10
+ aspect?: string;
11
+ };
12
+
13
+ export const PRODUCT_KEY = "Product";
14
+
15
+ export type ProductCardProps = {
16
+ node?: TSlateProduct;
17
+ style?: CSSProperties;
18
+ };
19
+
20
+ // Slate用的渲染组件
21
+ export const ProductCard = ({ node }: ProductCardProps) => {
22
+ return <ProductView product={node?.product} node={node} />;
23
+ };
@@ -0,0 +1,12 @@
1
+ import { useQueryProduct } from "./useQueryProduct";
2
+ import { ProductCardProps } from "./ProductCard";
3
+ import { ProductView } from "./ProductView";
4
+
5
+
6
+ export const createXProductCardPreview = (websiteId?: string) => {
7
+ return ({ node }: ProductCardProps) => {
8
+ const product = useQueryProduct(node?.productId, websiteId);
9
+
10
+ return <ProductView product={product} node={node} />;
11
+ }
12
+ }
@@ -0,0 +1,41 @@
1
+ import { Product } from "@rxdrag/rxcms-models";
2
+ import "./style.css";
3
+ import {
4
+ DATA_POPUP,
5
+ DATA_POPUP_CTA,
6
+ DATA_POPUP_ROLE,
7
+ DEFAULT_ENQUIRY_POPUP,
8
+ openPopup,
9
+ PopupRole,
10
+ } from "../../../../scripts";
11
+ import { useRef } from "react";
12
+
13
+ //TODO: 跟询盘触发器同样的实现原理,给询盘对话框发消息
14
+ export function ProductCta(props: { product?: Product; popupKey?: string }) {
15
+ const { product, popupKey = DEFAULT_ENQUIRY_POPUP } = props;
16
+ const ref = useRef<HTMLButtonElement>(null);
17
+ const roleProps = {
18
+ [DATA_POPUP_CTA]: `From RichText ProductCard-${product?.title}-${product?.id}`,
19
+ [DATA_POPUP_ROLE]: PopupRole.ModalTrigger,
20
+ [DATA_POPUP]: popupKey,
21
+ };
22
+
23
+ const handleClick = () => {
24
+ if (ref.current) {
25
+ openPopup(roleProps[DATA_POPUP_CTA] || popupKey, ref.current);
26
+ }
27
+ };
28
+
29
+ return (
30
+ <div className="flex justify-center items-center">
31
+ <button
32
+ ref={ref}
33
+ {...roleProps}
34
+ className="product-cta-button relative flex-1 mt-2 flex items-center justify-center rounded-md border border-transparent bg-sky-600 text-md font-smibold text-white hover:bg-sky-700"
35
+ onClick={handleClick}
36
+ >
37
+ Get a quote
38
+ </button>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,4 @@
1
+ .product-cta-button {
2
+ width: 100%;
3
+ padding: 8px 16px;
4
+ }
@@ -0,0 +1,13 @@
1
+ import clsx from "clsx"
2
+ import { HTMLAttributes } from "react"
3
+ import "./style.css"
4
+
5
+ export function ProductDescription(props: HTMLAttributes<HTMLParagraphElement>) {
6
+ const { className, ...rest } = props
7
+ return (
8
+ <p
9
+ className={clsx("product-description", className)}
10
+ {...rest}
11
+ />
12
+ )
13
+ }
@@ -0,0 +1,6 @@
1
+ .x-figure .product-description {
2
+ margin-top: 0;
3
+ margin-bottom: 0;
4
+ padding-top: 0;
5
+ padding-bottom: 0.4rem;
6
+ }
@@ -0,0 +1,34 @@
1
+ import { Product } from "@rxdrag/rxcms-models";
2
+ import { HtmlHTMLAttributes } from "react";
3
+ import "./style.css";
4
+
5
+ export function ProductMedia(
6
+ props: {
7
+ product?: Product;
8
+ width?: number;
9
+ aspect?: string;
10
+ } & HtmlHTMLAttributes<HTMLImageElement>
11
+ ) {
12
+ const { product, style, width, className, aspect, ...rest } = props;
13
+ const media = product?.mediaPivots?.[0]?.media;
14
+
15
+ return (
16
+ <div
17
+ className={"product-media " + className}
18
+ style={{
19
+ aspectRatio: aspect || "4 / 3",
20
+ ...style,
21
+ width,
22
+ }}
23
+ {...rest}
24
+ >
25
+ <img
26
+ style={{
27
+ width: "100%",
28
+ }}
29
+ src={media?.file?.resize || media?.file?.url || media?.file?.thumbnail}
30
+ alt={product?.mediaPivots?.[0]?.altText}
31
+ />
32
+ </div>
33
+ );
34
+ }