@rxdrag/website-lib-core 0.0.64 → 0.0.65
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/package.json +3 -3
- package/src/react/components/ContactForm/TelInput2.tsx +181 -145
- package/src/react/components/ReactModalTrigger.tsx +44 -0
- package/src/react/components/ReactVideoPlayer.tsx +255 -0
- package/src/react/components/index.ts +3 -1
- package/src/react/components/VideoPlayer.tsx +0 -146
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rxdrag/website-lib-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.65",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./index.ts"
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"@types/react-dom": "^19.1.0",
|
|
25
25
|
"eslint": "^7.32.0",
|
|
26
26
|
"typescript": "^5",
|
|
27
|
-
"@rxdrag/
|
|
27
|
+
"@rxdrag/slate-preview": "1.2.58",
|
|
28
28
|
"@rxdrag/tsconfig": "0.2.0",
|
|
29
|
-
"@rxdrag/
|
|
29
|
+
"@rxdrag/eslint-config-custom": "0.2.12"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"clsx": "^2.1.0",
|
|
@@ -14,164 +14,200 @@ export interface TelInputProps
|
|
|
14
14
|
labelClassName?: string;
|
|
15
15
|
inputClassName?: string;
|
|
16
16
|
countries?: CountryDial[];
|
|
17
|
+
selectPlaceholder?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export const TelInput2 = forwardRef<HTMLInputElement, TelInputProps>(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
export const TelInput2 = forwardRef<HTMLInputElement, TelInputProps>(
|
|
21
|
+
(props, ref) => {
|
|
22
|
+
const {
|
|
23
|
+
label,
|
|
24
|
+
name,
|
|
25
|
+
required,
|
|
26
|
+
requiredClassName,
|
|
27
|
+
className,
|
|
28
|
+
labelClassName,
|
|
29
|
+
inputClassName,
|
|
30
|
+
countries,
|
|
31
|
+
placeholder,
|
|
32
|
+
selectPlaceholder,
|
|
33
|
+
value,
|
|
34
|
+
onChange,
|
|
35
|
+
} = props;
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
const { labelRef, paddingLeft } = useInlineLabelPadding(16);
|
|
38
|
+
const {
|
|
39
|
+
allCountries,
|
|
40
|
+
selected,
|
|
41
|
+
localNumber,
|
|
42
|
+
onSelectCountry,
|
|
43
|
+
onChangeLocal,
|
|
44
|
+
} = useTelControl({ countries, name, value, onChange });
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
const [open, setOpen] = useState(false);
|
|
47
|
+
const [query, setQuery] = useState("");
|
|
48
|
+
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
49
|
+
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
const onDocClick = (e: MouseEvent) => {
|
|
61
|
-
const target = e.target as Node;
|
|
62
|
-
if (
|
|
63
|
-
open &&
|
|
64
|
-
panelRef.current &&
|
|
65
|
-
!panelRef.current.contains(target) &&
|
|
66
|
-
buttonRef.current &&
|
|
67
|
-
!buttonRef.current.contains(target)
|
|
68
|
-
) {
|
|
69
|
-
setOpen(false);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
document.addEventListener("mousedown", onDocClick);
|
|
73
|
-
return () => document.removeEventListener("mousedown", onDocClick);
|
|
74
|
-
}, [open]);
|
|
51
|
+
const filtered = useMemo(() => {
|
|
52
|
+
if (!query) return allCountries;
|
|
53
|
+
const q = query.toLowerCase();
|
|
54
|
+
return allCountries.filter(
|
|
55
|
+
(c) =>
|
|
56
|
+
c.name.toLowerCase().includes(q) ||
|
|
57
|
+
(c.region && c.region.toLowerCase().includes(q)) ||
|
|
58
|
+
c.dialCode.replace("+", "").includes(q.replace("+", ""))
|
|
59
|
+
);
|
|
60
|
+
}, [allCountries, query]);
|
|
75
61
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const onDocClick = (e: MouseEvent) => {
|
|
64
|
+
const target = e.target as Node;
|
|
65
|
+
if (
|
|
66
|
+
open &&
|
|
67
|
+
panelRef.current &&
|
|
68
|
+
!panelRef.current.contains(target) &&
|
|
69
|
+
buttonRef.current &&
|
|
70
|
+
!buttonRef.current.contains(target)
|
|
71
|
+
) {
|
|
72
|
+
setOpen(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
document.addEventListener("mousedown", onDocClick);
|
|
76
|
+
return () => document.removeEventListener("mousedown", onDocClick);
|
|
77
|
+
}, [open]);
|
|
89
78
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
onClick={() => setOpen((v) => !v)}
|
|
104
|
-
className={"h-full min-w-[100px] inline-flex items-center justify-between whitespace-nowrap rounded-md px-3 py-0 text-sm"}
|
|
105
|
-
>
|
|
106
|
-
<span className="truncate text-left leading-tight">
|
|
107
|
-
{selected ? (
|
|
108
|
-
<span className="font-medium">{selected.dialCode}</span>
|
|
109
|
-
) : (
|
|
110
|
-
<span className="text-gray-500">+ Code</span>
|
|
111
|
-
)}
|
|
112
|
-
</span>
|
|
113
|
-
<svg className="ml-2 h-4 w-4 text-gray-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
114
|
-
<path fillRule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.085l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0L5.25 8.27a.75.75 0 01-.02-1.06z" clipRule="evenodd" />
|
|
115
|
-
</svg>
|
|
116
|
-
</button>
|
|
79
|
+
return (
|
|
80
|
+
<div className={clsx("relative w-full", className)}>
|
|
81
|
+
<label
|
|
82
|
+
ref={labelRef}
|
|
83
|
+
htmlFor={name}
|
|
84
|
+
className={clsx(
|
|
85
|
+
"absolute left-3 top-1/2 -translate-y-1/2 mt-1 opacity-70 text-sm pointer-events-none whitespace-nowrap",
|
|
86
|
+
labelClassName
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{label}
|
|
90
|
+
{required ? <span className={requiredClassName}>*</span> : null}
|
|
91
|
+
</label>
|
|
117
92
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
93
|
+
<div
|
|
94
|
+
className={clsx(
|
|
95
|
+
"flex h-10 items-center gap-2 rounded-md shadow-sm focus-within:ring-2 focus-within:ring-primary-300",
|
|
96
|
+
inputClassName
|
|
97
|
+
)}
|
|
98
|
+
style={{ paddingLeft }}
|
|
99
|
+
>
|
|
100
|
+
<div className="relative h-full">
|
|
101
|
+
<button
|
|
102
|
+
ref={buttonRef}
|
|
103
|
+
type="button"
|
|
104
|
+
aria-haspopup="listbox"
|
|
105
|
+
aria-expanded={open}
|
|
106
|
+
onClick={() => setOpen((v) => !v)}
|
|
107
|
+
className={
|
|
108
|
+
"h-full min-w-[100px] inline-flex items-center justify-between whitespace-nowrap rounded-md px-3 py-0 text-sm"
|
|
109
|
+
}
|
|
110
|
+
>
|
|
111
|
+
<span className="truncate text-left leading-tight">
|
|
112
|
+
{selected ? (
|
|
113
|
+
<span className="font-medium">{selected.dialCode}</span>
|
|
114
|
+
) : (
|
|
115
|
+
<span className="text-gray-500">+ Code</span>
|
|
116
|
+
)}
|
|
117
|
+
</span>
|
|
118
|
+
<svg
|
|
119
|
+
className="ml-2 h-4 w-4 text-gray-500"
|
|
120
|
+
viewBox="0 0 20 20"
|
|
121
|
+
fill="currentColor"
|
|
122
|
+
aria-hidden="true"
|
|
123
|
+
>
|
|
124
|
+
<path
|
|
125
|
+
fillRule="evenodd"
|
|
126
|
+
d="M5.23 7.21a.75.75 0 011.06.02L10 11.085l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0L5.25 8.27a.75.75 0 01-.02-1.06z"
|
|
127
|
+
clipRule="evenodd"
|
|
126
128
|
/>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
</svg>
|
|
130
|
-
</div>
|
|
129
|
+
</svg>
|
|
130
|
+
</button>
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
132
|
+
{open ? (
|
|
133
|
+
<div
|
|
134
|
+
ref={panelRef}
|
|
135
|
+
className="absolute z-50 mt-2 w-[320px] rounded-md border border-gray-200 bg-white p-2 shadow-lg"
|
|
136
|
+
>
|
|
137
|
+
<div className="flex items-center gap-2 rounded-md border border-gray-200 px-2 py-1.5">
|
|
138
|
+
<input
|
|
139
|
+
value={query}
|
|
140
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
141
|
+
placeholder={selectPlaceholder || "Enter Country/Region"}
|
|
142
|
+
className="h-8 w-full bg-transparent text-sm outline-none"
|
|
143
|
+
/>
|
|
144
|
+
<svg
|
|
145
|
+
className="h-4 w-4 text-gray-500"
|
|
146
|
+
viewBox="0 0 20 20"
|
|
147
|
+
fill="currentColor"
|
|
148
|
+
aria-hidden="true"
|
|
142
149
|
>
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
<path
|
|
151
|
+
fillRule="evenodd"
|
|
152
|
+
d="M8.5 3.5a5 5 0 013.996 8.1l3.202 3.203a.75.75 0 11-1.06 1.06l-3.203-3.202A5 5 0 118.5 3.5zm0 1.5a3.5 3.5 0 100 7 3.5 3.5 0 000-7z"
|
|
153
|
+
clipRule="evenodd"
|
|
154
|
+
/>
|
|
155
|
+
</svg>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="mt-2 max-h-64 overflow-auto pr-1">
|
|
159
|
+
{filtered.map((c) => (
|
|
160
|
+
<button
|
|
161
|
+
key={c.code}
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={() => {
|
|
164
|
+
onSelectCountry(c);
|
|
165
|
+
setOpen(false);
|
|
166
|
+
}}
|
|
167
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm hover:bg-gray-50"
|
|
168
|
+
>
|
|
169
|
+
<span
|
|
170
|
+
className={clsx(
|
|
171
|
+
"inline-block h-3 w-3 rounded-full border",
|
|
172
|
+
selected?.code === c.code
|
|
173
|
+
? "border-primary-500 ring-4 ring-primary-200"
|
|
174
|
+
: "border-gray-400"
|
|
175
|
+
)}
|
|
176
|
+
></span>
|
|
177
|
+
<span className="flex-1 truncate">
|
|
178
|
+
{c.name}
|
|
179
|
+
{c.region ? (
|
|
180
|
+
<span className="text-gray-500">, {c.region}</span>
|
|
181
|
+
) : null}
|
|
182
|
+
</span>
|
|
183
|
+
<span className="text-gray-700">{c.dialCode}</span>
|
|
184
|
+
</button>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
151
187
|
</div>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
</div>
|
|
188
|
+
) : null}
|
|
189
|
+
</div>
|
|
155
190
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
<input
|
|
192
|
+
ref={ref}
|
|
193
|
+
id={name}
|
|
194
|
+
name={name}
|
|
195
|
+
required={required}
|
|
196
|
+
placeholder={placeholder || "Phone number"}
|
|
197
|
+
inputMode="tel"
|
|
198
|
+
className={clsx(
|
|
199
|
+
"h-10 w-full appearance-none rounded-md border-0 bg-transparent px-3 py-0 text-sm leading-tight focus:outline-none"
|
|
200
|
+
)}
|
|
201
|
+
value={localNumber}
|
|
202
|
+
onChange={(e) => onChangeLocal(e.target.value)}
|
|
203
|
+
autoComplete={name}
|
|
204
|
+
autoCorrect="off"
|
|
205
|
+
spellCheck={false}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
172
208
|
</div>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
);
|
|
176
212
|
|
|
177
213
|
export default TelInput2;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
DATA_POPUP_CTA,
|
|
4
|
+
DATA_OPENABLE_ROLE,
|
|
5
|
+
OpenAble,
|
|
6
|
+
DATA_OPENABLE,
|
|
7
|
+
popover,
|
|
8
|
+
} from "../../controller";
|
|
9
|
+
|
|
10
|
+
export function ReactModalTrigger(props: {
|
|
11
|
+
className?: string;
|
|
12
|
+
openableKey?: string;
|
|
13
|
+
callToAction?: string;
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
}) {
|
|
16
|
+
const {
|
|
17
|
+
className,
|
|
18
|
+
callToAction,
|
|
19
|
+
openableKey = "enquiry-modal",
|
|
20
|
+
children,
|
|
21
|
+
} = props;
|
|
22
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
23
|
+
const roleProps = {
|
|
24
|
+
[DATA_POPUP_CTA]: callToAction,
|
|
25
|
+
[DATA_OPENABLE_ROLE]: OpenAble.ModalTrigger,
|
|
26
|
+
[DATA_OPENABLE]: openableKey,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleClick = () => {
|
|
30
|
+
if (ref.current) {
|
|
31
|
+
popover.open(openableKey, ref.current);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
ref={ref}
|
|
37
|
+
{...roleProps}
|
|
38
|
+
className={className}
|
|
39
|
+
onClick={handleClick}
|
|
40
|
+
>
|
|
41
|
+
{children || "Get a quote"}
|
|
42
|
+
</button>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import Hls from "hls.js";
|
|
3
|
+
import type { Media } from "@rxdrag/rxcms-models";
|
|
4
|
+
import { ReactModalTrigger } from "./ReactModalTrigger";
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
|
|
7
|
+
export type ID = string | number;
|
|
8
|
+
|
|
9
|
+
export type VideoPlayerClassNames = {
|
|
10
|
+
container?: string;
|
|
11
|
+
video?: string;
|
|
12
|
+
playButton?: string;
|
|
13
|
+
playButtonOuter?: string;
|
|
14
|
+
playButtonInner?: string;
|
|
15
|
+
playIcon?: string;
|
|
16
|
+
overlay?: string;
|
|
17
|
+
overlayTitle?: string;
|
|
18
|
+
ctaButton?: string;
|
|
19
|
+
overlayReplayButton?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function ReactVideoPlayer(props: {
|
|
23
|
+
endTitle?: string;
|
|
24
|
+
media: Media;
|
|
25
|
+
posterUrl?: string;
|
|
26
|
+
classNames?: VideoPlayerClassNames;
|
|
27
|
+
callToAction?: string;
|
|
28
|
+
onToggleSelect?: (id: ID) => void;
|
|
29
|
+
}) {
|
|
30
|
+
const {
|
|
31
|
+
endTitle,
|
|
32
|
+
media,
|
|
33
|
+
onToggleSelect,
|
|
34
|
+
posterUrl,
|
|
35
|
+
classNames,
|
|
36
|
+
callToAction,
|
|
37
|
+
} = props;
|
|
38
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
39
|
+
const hlsRef = useRef<Hls | null>(null);
|
|
40
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
41
|
+
const [isEnded, setIsEnded] = useState(false);
|
|
42
|
+
|
|
43
|
+
const handleContainerClick = useCallback(
|
|
44
|
+
(e: React.MouseEvent) => {
|
|
45
|
+
if (isPlaying && videoRef.current) {
|
|
46
|
+
e.stopPropagation();
|
|
47
|
+
videoRef.current.pause();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (media.id != null) {
|
|
51
|
+
onToggleSelect?.(media.id);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
[isPlaying, media.id, onToggleSelect]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const handlePlayClick = useCallback(
|
|
58
|
+
(e: React.MouseEvent) => {
|
|
59
|
+
e.stopPropagation();
|
|
60
|
+
const v = videoRef.current;
|
|
61
|
+
if (!v) return;
|
|
62
|
+
if (isPlaying) v.pause();
|
|
63
|
+
else v.play();
|
|
64
|
+
},
|
|
65
|
+
[isPlaying]
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const video = videoRef.current;
|
|
70
|
+
const url = media.file?.original;
|
|
71
|
+
if (!video || !url) return;
|
|
72
|
+
|
|
73
|
+
const onPlay = () => {
|
|
74
|
+
setIsPlaying(true);
|
|
75
|
+
setIsEnded(false);
|
|
76
|
+
};
|
|
77
|
+
const onPause = () => setIsPlaying(false);
|
|
78
|
+
const onEnded = () => {
|
|
79
|
+
setIsPlaying(false);
|
|
80
|
+
setIsEnded(true);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
video.addEventListener("play", onPlay);
|
|
84
|
+
video.addEventListener("pause", onPause);
|
|
85
|
+
video.addEventListener("ended", onEnded);
|
|
86
|
+
|
|
87
|
+
if (media.storageType === "cloudflare_stream") {
|
|
88
|
+
if (!Hls.isSupported()) {
|
|
89
|
+
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
90
|
+
video.src = url;
|
|
91
|
+
}
|
|
92
|
+
return () => {
|
|
93
|
+
video.removeEventListener("play", onPlay);
|
|
94
|
+
video.removeEventListener("pause", onPause);
|
|
95
|
+
video.removeEventListener("ended", onEnded);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const hls = new Hls({ enableWorker: true });
|
|
100
|
+
hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
101
|
+
if (data.fatal) {
|
|
102
|
+
switch (data.type) {
|
|
103
|
+
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
104
|
+
hls.startLoad();
|
|
105
|
+
break;
|
|
106
|
+
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
107
|
+
hls.recoverMediaError();
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
hls.destroy();
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
hls.loadSource(url);
|
|
117
|
+
hls.attachMedia(video);
|
|
118
|
+
hlsRef.current = hls;
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
video.removeEventListener("play", onPlay);
|
|
122
|
+
video.removeEventListener("pause", onPause);
|
|
123
|
+
video.removeEventListener("ended", onEnded);
|
|
124
|
+
hls.destroy();
|
|
125
|
+
hlsRef.current = null;
|
|
126
|
+
};
|
|
127
|
+
} else {
|
|
128
|
+
video.src = url;
|
|
129
|
+
return () => {
|
|
130
|
+
video.removeEventListener("play", onPlay);
|
|
131
|
+
video.removeEventListener("pause", onPause);
|
|
132
|
+
video.removeEventListener("ended", onEnded);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}, [media.file?.original, media.storageType]);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
className={[
|
|
140
|
+
"relative w-full aspect-video rounded-2xl",
|
|
141
|
+
classNames?.container,
|
|
142
|
+
]
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.join(" ")}
|
|
145
|
+
onClick={handleContainerClick}
|
|
146
|
+
>
|
|
147
|
+
<video
|
|
148
|
+
ref={videoRef}
|
|
149
|
+
preload="metadata"
|
|
150
|
+
poster={posterUrl ?? media.file?.thumbnail}
|
|
151
|
+
className={["w-full h-full rounded-lg object-cover", classNames?.video]
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.join(" ")}
|
|
154
|
+
controls
|
|
155
|
+
>
|
|
156
|
+
{!media.storageType && media.file?.original ? (
|
|
157
|
+
<source src={media.file.original} />
|
|
158
|
+
) : null}
|
|
159
|
+
Your browser does not support video playback.
|
|
160
|
+
</video>
|
|
161
|
+
|
|
162
|
+
{!isPlaying && !isEnded && (
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={handlePlayClick}
|
|
166
|
+
className={[
|
|
167
|
+
"absolute inset-0 flex items-center justify-center w-full h-full transition rounded-lg z-10",
|
|
168
|
+
classNames?.playButton,
|
|
169
|
+
]
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
.join(" ")}
|
|
172
|
+
>
|
|
173
|
+
<div
|
|
174
|
+
className={[
|
|
175
|
+
"flex items-center justify-center size-[130px] bg-white/15 rounded-full backdrop-blur-sm hover:shadow-md transition-all duration-300 hover:scale-110 group",
|
|
176
|
+
classNames?.playButtonOuter,
|
|
177
|
+
]
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.join(" ")}
|
|
180
|
+
>
|
|
181
|
+
<div
|
|
182
|
+
className={[
|
|
183
|
+
"size-[90px] bg-white relative overflow-hidden rounded-full",
|
|
184
|
+
classNames?.playButtonInner,
|
|
185
|
+
]
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join(" ")}
|
|
188
|
+
>
|
|
189
|
+
<div className="absolute inset-0 cross-gradient box-shadow-1">
|
|
190
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
191
|
+
<svg
|
|
192
|
+
className={["size-8 text-gray-700", classNames?.playIcon]
|
|
193
|
+
.filter(Boolean)
|
|
194
|
+
.join(" ")}
|
|
195
|
+
viewBox="0 0 30 32"
|
|
196
|
+
fill="currentColor"
|
|
197
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
198
|
+
>
|
|
199
|
+
<path d="M27.2003 19.4972C29.945 17.9735 29.945 14.0264 27.2003 12.5027L6.44148 0.978573C3.77538 -0.501503 0.5 1.42643 0.5 4.47581V27.5241C0.5 30.5735 3.77537 32.5014 6.44147 31.0214L27.2003 19.4972Z" />
|
|
200
|
+
</svg>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</button>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
<div
|
|
209
|
+
className={[
|
|
210
|
+
"absolute inset-0 bg-black/50 flex-col gap-6 items-center justify-center text-center text-white hidden z-10 rounded-2xl",
|
|
211
|
+
classNames?.overlay,
|
|
212
|
+
]
|
|
213
|
+
.filter(Boolean)
|
|
214
|
+
.join(" ")}
|
|
215
|
+
style={{ display: isEnded ? "flex" : "none" }}
|
|
216
|
+
>
|
|
217
|
+
<p
|
|
218
|
+
className={clsx("text-4xl drop-shadow-lg", classNames?.overlayTitle)}
|
|
219
|
+
>
|
|
220
|
+
{endTitle ?? "Contact Us Now"}
|
|
221
|
+
</p>
|
|
222
|
+
<div className="flex items-center gap-4">
|
|
223
|
+
<ReactModalTrigger
|
|
224
|
+
className={clsx(
|
|
225
|
+
"btn btn-primary px-10 py-2.5 rounded-full ripple",
|
|
226
|
+
classNames?.ctaButton
|
|
227
|
+
)}
|
|
228
|
+
callToAction={callToAction}
|
|
229
|
+
>
|
|
230
|
+
Consult Now
|
|
231
|
+
</ReactModalTrigger>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
className={[
|
|
235
|
+
"px-10 py-2.5 bg-white/20 hover:bg-white/30 rounded-full transition text-sm",
|
|
236
|
+
classNames?.overlayReplayButton,
|
|
237
|
+
]
|
|
238
|
+
.filter(Boolean)
|
|
239
|
+
.join(" ")}
|
|
240
|
+
onClick={(e) => {
|
|
241
|
+
e.stopPropagation();
|
|
242
|
+
const v = videoRef.current;
|
|
243
|
+
if (!v) return;
|
|
244
|
+
v.currentTime = 0;
|
|
245
|
+
setIsEnded(false);
|
|
246
|
+
v.play();
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
Replay
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
@@ -11,5 +11,7 @@ export * from "./SearchInput";
|
|
|
11
11
|
export * from "./ToTop";
|
|
12
12
|
export * from "./BackgroundVideoPlayer";
|
|
13
13
|
export * from "./BackgroundHlsVideoPlayer";
|
|
14
|
-
export * from "./
|
|
14
|
+
export * from "./ReactModalTrigger";
|
|
15
|
+
export * from "./ReactVideoPlayer";
|
|
16
|
+
|
|
15
17
|
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { ID } from "@rxdrag/entify-lib";
|
|
2
|
-
import { Media } from "@rxdrag/rxcms-models";
|
|
3
|
-
import React, { useEffect, useRef, useState } from "react";
|
|
4
|
-
import Hls from "hls.js";
|
|
5
|
-
import { Icon } from "@iconify/react";
|
|
6
|
-
|
|
7
|
-
export function VideoPlayer(props: {
|
|
8
|
-
media: Media;
|
|
9
|
-
onToggleSelect?: (id: ID) => void;
|
|
10
|
-
}) {
|
|
11
|
-
const { media, onToggleSelect } = props;
|
|
12
|
-
const videoRef = useRef<HTMLVideoElement>(null);
|
|
13
|
-
const hlsRef = useRef<Hls | null>(null);
|
|
14
|
-
const [isPlaying, setIsPlaying] = useState(false);
|
|
15
|
-
|
|
16
|
-
const handleContainerClick = React.useCallback(
|
|
17
|
-
(e: React.MouseEvent) => {
|
|
18
|
-
// 如果正在播放,点击暂停并阻止事件冒泡(避免同时触发选择)
|
|
19
|
-
if (isPlaying && videoRef.current) {
|
|
20
|
-
e.stopPropagation();
|
|
21
|
-
videoRef.current.pause();
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// 未播放时,触发选择事件
|
|
26
|
-
onToggleSelect?.(media.id!);
|
|
27
|
-
},
|
|
28
|
-
[media.id, onToggleSelect, isPlaying]
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
const handlePlayClick = React.useCallback(
|
|
32
|
-
(e: React.MouseEvent) => {
|
|
33
|
-
e.stopPropagation();
|
|
34
|
-
if (videoRef.current) {
|
|
35
|
-
if (isPlaying) {
|
|
36
|
-
videoRef.current.pause();
|
|
37
|
-
} else {
|
|
38
|
-
videoRef.current.play();
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
[isPlaying]
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (!videoRef.current || !media.file?.original) return;
|
|
47
|
-
|
|
48
|
-
const video = videoRef.current;
|
|
49
|
-
const videoUrl = media.file.original;
|
|
50
|
-
|
|
51
|
-
// 监听播放状态
|
|
52
|
-
const handlePlay = () => {
|
|
53
|
-
setIsPlaying(true);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const handlePause = () => {
|
|
57
|
-
setIsPlaying(false);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
video.addEventListener("play", handlePlay);
|
|
61
|
-
video.addEventListener("pause", handlePause);
|
|
62
|
-
|
|
63
|
-
// Cloudflare Stream 使用 HLS
|
|
64
|
-
if (media.storageType === "cloudflare_stream") {
|
|
65
|
-
if (!Hls.isSupported()) {
|
|
66
|
-
// Safari 原生支持 HLS
|
|
67
|
-
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
68
|
-
video.src = videoUrl;
|
|
69
|
-
}
|
|
70
|
-
return () => {
|
|
71
|
-
video.removeEventListener("play", handlePlay);
|
|
72
|
-
video.removeEventListener("pause", handlePause);
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const hls = new Hls({
|
|
77
|
-
enableWorker: true,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
hls.on(Hls.Events.ERROR, (event, data) => {
|
|
81
|
-
if (data.fatal) {
|
|
82
|
-
switch (data.type) {
|
|
83
|
-
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
84
|
-
hls.startLoad();
|
|
85
|
-
break;
|
|
86
|
-
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
87
|
-
hls.recoverMediaError();
|
|
88
|
-
break;
|
|
89
|
-
default:
|
|
90
|
-
hls.destroy();
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
hls.loadSource(videoUrl);
|
|
97
|
-
hls.attachMedia(video);
|
|
98
|
-
hlsRef.current = hls;
|
|
99
|
-
|
|
100
|
-
return () => {
|
|
101
|
-
video.removeEventListener("play", handlePlay);
|
|
102
|
-
video.removeEventListener("pause", handlePause);
|
|
103
|
-
hls.destroy();
|
|
104
|
-
hlsRef.current = null;
|
|
105
|
-
};
|
|
106
|
-
} else {
|
|
107
|
-
// 普通视频使用原生 video
|
|
108
|
-
video.src = videoUrl;
|
|
109
|
-
|
|
110
|
-
return () => {
|
|
111
|
-
video.removeEventListener("play", handlePlay);
|
|
112
|
-
video.removeEventListener("pause", handlePause);
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
}, [media.file?.original, media.storageType]);
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<div
|
|
119
|
-
className="z-0 w-full bg-default-100 cursor-pointer aspect-square relative group"
|
|
120
|
-
onClick={handleContainerClick}
|
|
121
|
-
>
|
|
122
|
-
<video
|
|
123
|
-
ref={videoRef}
|
|
124
|
-
preload="metadata"
|
|
125
|
-
poster={media.file?.thumbnail}
|
|
126
|
-
className="absolute inset-0 w-full h-full object-cover"
|
|
127
|
-
/>
|
|
128
|
-
|
|
129
|
-
{/* 播放按钮 - 播放时隐藏 */}
|
|
130
|
-
{!isPlaying && (
|
|
131
|
-
<button
|
|
132
|
-
onClick={handlePlayClick}
|
|
133
|
-
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 cursor-pointer"
|
|
134
|
-
>
|
|
135
|
-
<div className="w-12 h-12 rounded-full bg-white/90 hover:bg-white flex items-center justify-center shadow-lg transition-all hover:scale-110">
|
|
136
|
-
<Icon
|
|
137
|
-
icon="mdi:play"
|
|
138
|
-
className="text-2xl text-gray-800"
|
|
139
|
-
style={{ marginLeft: "3px" }}
|
|
140
|
-
/>
|
|
141
|
-
</div>
|
|
142
|
-
</button>
|
|
143
|
-
)}
|
|
144
|
-
</div>
|
|
145
|
-
);
|
|
146
|
-
}
|