@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.
- package/index.ts +1 -0
- package/package.json +12 -13
- package/src/entify/Entify.ts +365 -0
- package/{dist/entify/index.d.ts → src/entify/index.ts} +4 -4
- package/src/entify/lib/createEntifyClient.ts +23 -0
- package/{dist/entify/lib/index.d.ts → src/entify/lib/index.ts} +29 -29
- package/src/entify/lib/langFields.ts +12 -0
- package/src/entify/lib/newAvatarQueryOptions.ts +5 -0
- package/src/entify/lib/newOgImageQueryOptions.ts +6 -0
- package/src/entify/lib/newPageMetaOptions.ts +20 -0
- package/src/entify/lib/newQueryPostOptions.ts +41 -0
- package/src/entify/lib/newQueryProductOptions.ts +90 -0
- package/src/entify/lib/newQueryProductsMediaOptions.ts +26 -0
- package/src/entify/lib/queryAllProducts.ts +27 -0
- package/src/entify/lib/queryEntityList.ts +44 -0
- package/src/entify/lib/queryFeaturedProducts.ts +47 -0
- package/src/entify/lib/queryLangs.ts +47 -0
- package/src/entify/lib/queryLatestPosts.ts +65 -0
- package/src/entify/lib/queryOneEntity.ts +67 -0
- package/src/entify/lib/queryOnePostById.ts +21 -0
- package/src/entify/lib/queryOnePostBySlug.ts +21 -0
- package/src/entify/lib/queryOnePostCategoryBySlug.ts +30 -0
- package/src/entify/lib/queryOneProductById.ts +20 -0
- package/src/entify/lib/queryOneProductBySlug.ts +21 -0
- package/src/entify/lib/queryOneProductCategoryBySlug.ts +30 -0
- package/src/entify/lib/queryOneTheme.ts +76 -0
- package/src/entify/lib/queryOneUser.ts +38 -0
- package/src/entify/lib/queryPostCategories.ts +48 -0
- package/src/entify/lib/queryPostSlugs.ts +32 -0
- package/src/entify/lib/queryPosts.ts +92 -0
- package/src/entify/lib/queryProductCategories.ts +44 -0
- package/src/entify/lib/queryProducts.ts +69 -0
- package/src/entify/lib/queryProductsInMenu.ts +31 -0
- package/src/entify/lib/queryUserIds.ts +24 -0
- package/src/entify/lib/queryUserPosts.ts +74 -0
- package/src/entify/lib/queryWebSiteSettings.ts +29 -0
- package/src/entify/lib/searchProducts.ts +70 -0
- package/src/entify/lib/sendEmail.ts +8 -0
- package/src/entify/lib/toQueryOptions.ts +20 -0
- package/src/entify/lib/upsertEntity.ts +9 -0
- package/src/entify/types/index.ts +2 -0
- package/src/entify/types/utils.ts +4 -0
- package/src/entify/types/variables.ts +7 -0
- package/src/entify/view-model/funcs.ts +271 -0
- package/src/entify/view-model/index.ts +2 -0
- package/src/entify/view-model/models.ts +143 -0
- package/{dist/index.d.ts → src/index.ts} +5 -5
- package/src/motion/consts.ts +598 -0
- package/{dist/motion/index.d.ts → src/motion/index.ts} +2 -2
- package/src/motion/types.ts +46 -0
- package/src/react/components/EnquiryForm/Input.tsx +52 -0
- package/src/react/components/EnquiryForm/Submit.tsx +30 -0
- package/src/react/components/EnquiryForm/Textarea.tsx +51 -0
- package/src/react/components/EnquiryForm/index.tsx +334 -0
- package/src/react/components/GoogleConsent/CookieItemPanel.tsx +81 -0
- package/src/react/components/GoogleConsent/CumtomizedModal.tsx +149 -0
- package/src/react/components/GoogleConsent/GoogleConsent.tsx +101 -0
- package/src/react/components/GoogleConsent/README.md +1 -0
- package/src/react/components/GoogleConsent/gtags.ts +68 -0
- package/src/react/components/GoogleConsent/index.ts +3 -0
- package/src/react/components/GoogleConsent/types.ts +18 -0
- package/src/react/components/GoogleConsent//345/217/202/350/200/203.md +4 -0
- package/src/react/components/Medias/index.tsx +347 -0
- package/src/react/components/ProductCard/ProductCard.tsx +23 -0
- package/src/react/components/ProductCard/ProductCardPreview.tsx +12 -0
- package/src/react/components/ProductCard/ProductCta/index.tsx +41 -0
- package/src/react/components/ProductCard/ProductCta/style.css +4 -0
- package/src/react/components/ProductCard/ProductDescription/index.tsx +13 -0
- package/src/react/components/ProductCard/ProductDescription/style.css +6 -0
- package/src/react/components/ProductCard/ProductMedia/index.tsx +34 -0
- package/src/react/components/ProductCard/ProductMedia/style.css +6 -0
- package/src/react/components/ProductCard/ProductTitle/index.tsx +7 -0
- package/src/react/components/ProductCard/ProductTitle/style.css +4 -0
- package/src/react/components/ProductCard/ProductView.tsx +35 -0
- package/{dist/react/components/ProductCard/index.d.ts → src/react/components/ProductCard/index.ts} +6 -6
- package/src/react/components/ProductCard/useQueryProduct.ts +32 -0
- package/src/react/components/RichTextOutline/index.tsx +76 -0
- package/src/react/components/RichTextOutline/useAcitviedHeading.ts +54 -0
- package/src/react/components/RichTextOutline/useAnchorScroll.ts +24 -0
- package/src/react/components/Scroller.tsx +7 -0
- package/src/react/components/SearchInput.tsx +34 -0
- package/src/react/components/Share/index.tsx +69 -0
- package/src/react/components/Share/socials.tsx +79 -0
- package/src/react/components/Share//350/265/204/346/226/231.md +7 -0
- package/src/react/components/ToTop/index.tsx +33 -0
- package/src/react/components/ToTop.tsx +33 -0
- package/{dist/react/components/index.d.ts → src/react/components/index.ts} +8 -8
- package/src/react/hooks/index.ts +1 -0
- package/src/react/hooks/useScroll.ts +23 -0
- package/{dist/react/index.d.ts → src/react/index.ts} +2 -2
- package/src/robots.ts +4 -0
- package/src/scripts/actions.ts +304 -0
- package/src/scripts/consts.ts +32 -0
- package/src/scripts/events.ts +33 -0
- package/{dist/scripts/index.d.ts → src/scripts/index.ts} +3 -3
- package/dist/entify/Entify.d.ts +0 -138
- package/dist/entify/lib/createEntifyClient.d.ts +0 -3
- package/dist/entify/lib/langFields.d.ts +0 -2
- package/dist/entify/lib/newAvatarQueryOptions.d.ts +0 -2
- package/dist/entify/lib/newOgImageQueryOptions.d.ts +0 -2
- package/dist/entify/lib/newPageMetaOptions.d.ts +0 -2
- package/dist/entify/lib/newQueryPostOptions.d.ts +0 -3
- package/dist/entify/lib/newQueryProductOptions.d.ts +0 -3
- package/dist/entify/lib/newQueryProductsMediaOptions.d.ts +0 -3
- package/dist/entify/lib/queryAllProducts.d.ts +0 -4
- package/dist/entify/lib/queryEntityList.d.ts +0 -3
- package/dist/entify/lib/queryFeaturedProducts.d.ts +0 -4
- package/dist/entify/lib/queryLangs.d.ts +0 -4
- package/dist/entify/lib/queryLatestPosts.d.ts +0 -4
- package/dist/entify/lib/queryOneEntity.d.ts +0 -5
- package/dist/entify/lib/queryOnePostById.d.ts +0 -3
- package/dist/entify/lib/queryOnePostBySlug.d.ts +0 -3
- package/dist/entify/lib/queryOnePostCategoryBySlug.d.ts +0 -3
- package/dist/entify/lib/queryOneProductById.d.ts +0 -3
- package/dist/entify/lib/queryOneProductBySlug.d.ts +0 -3
- package/dist/entify/lib/queryOneProductCategoryBySlug.d.ts +0 -3
- package/dist/entify/lib/queryOneTheme.d.ts +0 -3
- package/dist/entify/lib/queryOneUser.d.ts +0 -3
- package/dist/entify/lib/queryPostCategories.d.ts +0 -4
- package/dist/entify/lib/queryPostSlugs.d.ts +0 -4
- package/dist/entify/lib/queryPosts.d.ts +0 -10
- package/dist/entify/lib/queryProductCategories.d.ts +0 -4
- package/dist/entify/lib/queryProducts.d.ts +0 -6
- package/dist/entify/lib/queryProductsInMenu.d.ts +0 -2
- package/dist/entify/lib/queryUserIds.d.ts +0 -4
- package/dist/entify/lib/queryUserPosts.d.ts +0 -9
- package/dist/entify/lib/queryWebSiteSettings.d.ts +0 -3
- package/dist/entify/lib/searchProducts.d.ts +0 -4
- package/dist/entify/lib/sendEmail.d.ts +0 -3
- package/dist/entify/lib/toQueryOptions.d.ts +0 -3
- package/dist/entify/lib/upsertEntity.d.ts +0 -2
- package/dist/entify/types/index.d.ts +0 -2
- package/dist/entify/types/utils.d.ts +0 -4
- package/dist/entify/types/variables.d.ts +0 -7
- package/dist/entify/view-model/funcs.d.ts +0 -20
- package/dist/entify/view-model/index.d.ts +0 -2
- package/dist/entify/view-model/models.d.ts +0 -119
- package/dist/index.mjs +0 -40514
- package/dist/index.mjs.map +0 -1
- package/dist/motion/consts.d.ts +0 -77
- package/dist/motion/types.d.ts +0 -26
- package/dist/react/components/EnquiryForm/Input.d.ts +0 -15
- package/dist/react/components/EnquiryForm/Submit.d.ts +0 -8
- package/dist/react/components/EnquiryForm/Textarea.d.ts +0 -13
- package/dist/react/components/EnquiryForm/index.d.ts +0 -22
- package/dist/react/components/Medias/index.d.ts +0 -8
- package/dist/react/components/ProductCard/ProductCard.d.ts +0 -15
- package/dist/react/components/ProductCard/ProductCardPreview.d.ts +0 -2
- package/dist/react/components/ProductCard/ProductCta/index.d.ts +0 -5
- package/dist/react/components/ProductCard/ProductDescription/index.d.ts +0 -2
- package/dist/react/components/ProductCard/ProductMedia/index.d.ts +0 -7
- package/dist/react/components/ProductCard/ProductTitle/index.d.ts +0 -2
- package/dist/react/components/ProductCard/ProductView.d.ts +0 -5
- package/dist/react/components/ProductCard/useQueryProduct.d.ts +0 -2
- package/dist/react/components/RichTextOutline/index.d.ts +0 -8
- package/dist/react/components/RichTextOutline/useAcitviedHeading.d.ts +0 -1
- package/dist/react/components/Scroller.d.ts +0 -3
- package/dist/react/components/SearchInput.d.ts +0 -2
- package/dist/react/components/Share/index.d.ts +0 -6
- package/dist/react/components/Share/socials.d.ts +0 -10
- package/dist/react/components/ToTop.d.ts +0 -5
- package/dist/react/hooks/index.d.ts +0 -1
- package/dist/react/hooks/useScroll.d.ts +0 -2
- package/dist/robots.d.ts +0 -2
- package/dist/scripts/actions.d.ts +0 -85
- package/dist/scripts/consts.d.ts +0 -21
- package/dist/scripts/events.d.ts +0 -11
- 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,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,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,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,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
|
+
}
|