@rxdrag/website-lib-core 0.0.7 → 0.0.8
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 -1
- package/package.json +10 -6
- package/src/component-logic/gsap.d.ts +4 -0
- package/src/component-logic/index.ts +8 -0
- package/src/component-logic/link-client.ts +33 -0
- package/src/component-logic/link.ts +50 -0
- package/src/component-logic/modal.ts +36 -0
- package/src/component-logic/motion.ts +272 -0
- package/src/component-logic/number.ts +45 -0
- package/src/component-logic/popover.ts +51 -0
- package/src/component-logic/tabs.ts +10 -0
- package/src/controller/AnimateController.ts +138 -0
- package/src/controller/AosController.ts +240 -0
- package/src/controller/FlipController.ts +339 -0
- package/src/controller/ModalController.ts +127 -0
- package/src/controller/NumberController.ts +161 -0
- package/src/controller/PageLoader.ts +163 -0
- package/src/controller/PopoverController.ts +116 -0
- package/src/controller/TabsController.ts +271 -0
- package/src/controller/applyAnimation.ts +86 -0
- package/src/controller/applyInitialState.ts +79 -0
- package/src/{scripts → controller}/consts.ts +0 -2
- package/src/controller/index.ts +9 -0
- package/src/controller/popup.ts +346 -0
- package/src/controller/utils.ts +48 -0
- package/src/entify/Entify.ts +354 -365
- package/src/entify/IEntify.ts +91 -0
- package/src/entify/index.ts +3 -2
- package/src/entify/lib/newQueryProductOptions.ts +2 -3
- package/src/entify/lib/newQueryProductsMediaOptions.ts +19 -18
- package/src/entify/lib/queryAllProducts.ts +11 -3
- package/src/entify/lib/queryFeaturedProducts.ts +3 -3
- package/src/entify/lib/queryLatestPosts.ts +2 -2
- package/src/entify/lib/queryOneTheme.ts +1 -1
- package/src/entify/lib/queryPostCategories.ts +3 -3
- package/src/entify/lib/queryPostSlugs.ts +2 -2
- package/src/entify/lib/queryPosts.ts +92 -92
- package/src/entify/lib/queryProductCategories.ts +3 -3
- package/src/entify/lib/queryProducts.ts +69 -69
- package/src/entify/lib/queryUserPosts.ts +2 -2
- package/src/entify/lib/searchProducts.ts +2 -2
- package/src/index.ts +3 -1
- package/src/lib/formatDate.ts +15 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/pagination.ts +114 -0
- package/src/lib/utils.ts +119 -0
- package/src/motion/consts.ts +428 -598
- package/src/motion/convertToGsapVars.ts +102 -0
- package/src/motion/index.ts +5 -1
- package/src/motion/normalizeAnimation.ts +28 -0
- package/src/motion/normalizeAosAnimation.ts +22 -0
- package/src/motion/normalizePopupAnimation.ts +24 -0
- package/src/motion/types.ts +133 -46
- package/src/react/components/AttachmentIcon/index.tsx +53 -0
- package/src/react/components/ContactForm/index.tsx +341 -0
- package/src/react/components/Icon/index.tsx +10 -0
- package/src/react/components/Medias/index.tsx +347 -347
- package/src/react/components/ProductCard/ProductCta/index.tsx +7 -5
- package/src/react/components/RichTextOutline/index.tsx +76 -76
- package/src/react/components/Scroller.tsx +5 -1
- package/src/react/components/SearchInput.tsx +36 -34
- package/src/react/components/ToTop.tsx +63 -28
- package/src/react/components/index.ts +3 -1
- package/src/react/hooks/useScroll.ts +16 -10
- package/src/react/components/EnquiryForm/index.tsx +0 -334
- package/src/scripts/actions.ts +0 -304
- package/src/scripts/events.ts +0 -33
- package/src/scripts/index.ts +0 -3
- /package/src/react/components/{EnquiryForm → ContactForm}/Input.tsx +0 -0
- /package/src/react/components/{EnquiryForm → ContactForm}/Submit.tsx +0 -0
- /package/src/react/components/{EnquiryForm → ContactForm}/Textarea.tsx +0 -0
|
@@ -1,347 +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
|
-
);
|
|
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
|
+
);
|