@slopmachine/react 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/index.d.mts +34 -6
- package/dist/index.d.ts +34 -6
- package/dist/index.js +185 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +197 -100
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/ErrorOverlay.tsx +91 -0
- package/src/components/SlopImage.tsx +54 -31
- package/src/components/SlopText.tsx +1 -1
- package/src/components/SlopVideo.tsx +52 -29
- package/src/index.ts +2 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { clsx, type ClassValue } from "clsx";
|
|
3
3
|
import { twMerge } from "tailwind-merge";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
buildVideoUrl,
|
|
7
|
+
createRenderUrlMonitor,
|
|
8
|
+
type SlopMachineError,
|
|
7
9
|
type SlopVideoOptions,
|
|
8
10
|
type VideoAspectRatio,
|
|
9
11
|
} from "@slopmachine/core";
|
|
12
|
+
import { ErrorOverlay, type SlopErrorFallback } from "./ErrorOverlay";
|
|
10
13
|
|
|
11
14
|
// Utility for Tailwind classes
|
|
12
15
|
function cn(...inputs: ClassValue[]) {
|
|
@@ -26,6 +29,17 @@ export interface SlopVideoProps
|
|
|
26
29
|
* If not provided, a default spinner and shimmer effect will be shown.
|
|
27
30
|
*/
|
|
28
31
|
loader?: React.ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* Content to display if the video fails to generate or load.
|
|
34
|
+
* Pass a node, or a function that receives the `SlopMachineError` and returns a node.
|
|
35
|
+
* If not provided, a default error message is shown in place of the video.
|
|
36
|
+
*/
|
|
37
|
+
errorFallback?: SlopErrorFallback;
|
|
38
|
+
/**
|
|
39
|
+
* Called once per URL when the video fails to generate or load.
|
|
40
|
+
* `error.status` holds the HTTP status when the API reported the failure.
|
|
41
|
+
*/
|
|
42
|
+
onGenerationError?: (error: SlopMachineError) => void;
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
/**
|
|
@@ -56,7 +70,7 @@ export interface SlopVideoProps
|
|
|
56
70
|
* />
|
|
57
71
|
* ```
|
|
58
72
|
*
|
|
59
|
-
* @version 0.
|
|
73
|
+
* @version 0.7.0
|
|
60
74
|
*/
|
|
61
75
|
export const SlopVideo: React.FC<SlopVideoProps> = ({
|
|
62
76
|
bucketId,
|
|
@@ -74,6 +88,8 @@ export const SlopVideo: React.FC<SlopVideoProps> = ({
|
|
|
74
88
|
original,
|
|
75
89
|
attachments,
|
|
76
90
|
loader,
|
|
91
|
+
errorFallback,
|
|
92
|
+
onGenerationError,
|
|
77
93
|
autoPlay = true,
|
|
78
94
|
loop = true,
|
|
79
95
|
muted = true,
|
|
@@ -127,40 +143,38 @@ export const SlopVideo: React.FC<SlopVideoProps> = ({
|
|
|
127
143
|
}, [rawSrc]);
|
|
128
144
|
|
|
129
145
|
const [isLoading, setIsLoading] = useState(true);
|
|
146
|
+
const [error, setError] = useState<SlopMachineError | null>(null);
|
|
130
147
|
const [currentSrc, setCurrentSrc] = useState(src);
|
|
131
148
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
132
|
-
|
|
133
149
|
if (src !== currentSrc) {
|
|
134
150
|
setCurrentSrc(src);
|
|
135
151
|
setIsLoading(true);
|
|
152
|
+
setError(null);
|
|
136
153
|
}
|
|
137
154
|
|
|
155
|
+
// Keep the latest callback without re-running effects when it changes identity
|
|
156
|
+
const onGenerationErrorRef = useRef(onGenerationError);
|
|
138
157
|
useEffect(() => {
|
|
139
|
-
|
|
158
|
+
onGenerationErrorRef.current = onGenerationError;
|
|
159
|
+
});
|
|
140
160
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const errRes = await fetch(src);
|
|
147
|
-
const errorText = await errRes.text();
|
|
148
|
-
const errorData = JSON.parse(errorText);
|
|
149
|
-
if (errorData.error) {
|
|
150
|
-
errorMessage = errorData.error;
|
|
151
|
-
}
|
|
152
|
-
} catch (e) {
|
|
153
|
-
// Failed to fetch or parse error details
|
|
154
|
-
}
|
|
155
|
-
console.error("SlopVideo error:", res.status, errorMessage);
|
|
156
|
-
setIsLoading(false);
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
.catch((err) => {
|
|
160
|
-
console.error(`Error fetching video from ${src}:`, err);
|
|
161
|
+
// Shares one URL check between the HEAD probe and the media element's error event
|
|
162
|
+
const monitor = useMemo(
|
|
163
|
+
() =>
|
|
164
|
+
createRenderUrlMonitor((err) => {
|
|
165
|
+
console.error("SlopVideo error:", err.status ?? "", err.message);
|
|
161
166
|
setIsLoading(false);
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
setError(err);
|
|
168
|
+
onGenerationErrorRef.current?.(err);
|
|
169
|
+
}),
|
|
170
|
+
[],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (!src) return;
|
|
175
|
+
monitor.watch(src);
|
|
176
|
+
return () => monitor.stop();
|
|
177
|
+
}, [src, monitor]);
|
|
164
178
|
|
|
165
179
|
return (
|
|
166
180
|
<>
|
|
@@ -313,6 +327,14 @@ export const SlopVideo: React.FC<SlopVideoProps> = ({
|
|
|
313
327
|
</>
|
|
314
328
|
)}
|
|
315
329
|
|
|
330
|
+
{error && (
|
|
331
|
+
<ErrorOverlay
|
|
332
|
+
error={error}
|
|
333
|
+
errorFallback={errorFallback}
|
|
334
|
+
label="Failed to load video"
|
|
335
|
+
/>
|
|
336
|
+
)}
|
|
337
|
+
|
|
316
338
|
{/* Video */}
|
|
317
339
|
<video
|
|
318
340
|
key={src}
|
|
@@ -328,19 +350,20 @@ export const SlopVideo: React.FC<SlopVideoProps> = ({
|
|
|
328
350
|
onLoadedData?.(e);
|
|
329
351
|
}}
|
|
330
352
|
onError={(e) => {
|
|
331
|
-
|
|
353
|
+
monitor.mediaFailed(src, "Failed to load video");
|
|
332
354
|
onError?.(e);
|
|
333
355
|
}}
|
|
356
|
+
aria-hidden={error ? true : props["aria-hidden"]}
|
|
334
357
|
className={cn(
|
|
335
358
|
"h-full w-full object-cover transition-opacity duration-500",
|
|
336
|
-
isLoading ? "opacity-0" : "opacity-100",
|
|
359
|
+
isLoading || error ? "opacity-0" : "opacity-100",
|
|
337
360
|
)}
|
|
338
361
|
style={{
|
|
339
362
|
height: "100%",
|
|
340
363
|
width: "100%",
|
|
341
364
|
objectFit: "cover",
|
|
342
365
|
transition: "opacity 500ms",
|
|
343
|
-
opacity: isLoading ? 0 : 1,
|
|
366
|
+
opacity: isLoading || error ? 0 : 1,
|
|
344
367
|
}}
|
|
345
368
|
/>
|
|
346
369
|
</div>
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { SlopImage, type SlopImageProps } from "./components/SlopImage";
|
|
2
2
|
export { SlopVideo, type SlopVideoProps } from "./components/SlopVideo";
|
|
3
3
|
export { SlopText, type SlopTextProps } from "./components/SlopText";
|
|
4
|
+
export type { SlopErrorFallback } from "./components/ErrorOverlay";
|
|
4
5
|
|
|
5
6
|
export type {
|
|
6
7
|
ImageAspectRatio,
|
|
@@ -18,4 +19,5 @@ export {
|
|
|
18
19
|
preloadVideo,
|
|
19
20
|
preloadText,
|
|
20
21
|
rateResult,
|
|
22
|
+
SlopMachineError,
|
|
21
23
|
} from "@slopmachine/core";
|