@vibehooks/react 0.0.1
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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +55 -0
- package/dist/useAsyncState.d.ts +52 -0
- package/dist/useAsyncState.js +173 -0
- package/dist/useAudio.d.ts +26 -0
- package/dist/useAudio.js +64 -0
- package/dist/useAutoScroll.d.ts +47 -0
- package/dist/useAutoScroll.js +122 -0
- package/dist/useBarcode.d.ts +77 -0
- package/dist/useBarcode.js +140 -0
- package/dist/useBatteryStatus.d.ts +53 -0
- package/dist/useBatteryStatus.js +67 -0
- package/dist/useBodyScrollFreeze.d.ts +36 -0
- package/dist/useBodyScrollFreeze.js +74 -0
- package/dist/useCameraCapture.d.ts +76 -0
- package/dist/useCameraCapture.js +116 -0
- package/dist/useCookies.d.ts +42 -0
- package/dist/useCookies.js +61 -0
- package/dist/useCopyToClipboard.d.ts +22 -0
- package/dist/useCopyToClipboard.js +31 -0
- package/dist/useCountDown.d.ts +80 -0
- package/dist/useCountDown.js +106 -0
- package/dist/useDebouncedState.d.ts +47 -0
- package/dist/useDebouncedState.js +47 -0
- package/dist/useExternalNotifications.d.ts +36 -0
- package/dist/useExternalNotifications.js +100 -0
- package/dist/useFile.d.ts +74 -0
- package/dist/useFile.js +74 -0
- package/dist/useFullScreen.d.ts +20 -0
- package/dist/useFullScreen.js +43 -0
- package/dist/useGeolocation.d.ts +47 -0
- package/dist/useGeolocation.js +68 -0
- package/dist/useHoverIntent.d.ts +45 -0
- package/dist/useHoverIntent.js +81 -0
- package/dist/useIdle.d.ts +47 -0
- package/dist/useIdle.js +59 -0
- package/dist/useIndexedDB.d.ts +60 -0
- package/dist/useIndexedDB.js +75 -0
- package/dist/useIntersectionObserver.d.ts +45 -0
- package/dist/useIntersectionObserver.js +70 -0
- package/dist/useIntervalSafe.d.ts +72 -0
- package/dist/useIntervalSafe.js +85 -0
- package/dist/useIsClient.d.ts +12 -0
- package/dist/useIsClient.js +21 -0
- package/dist/useIsDesktop.d.ts +12 -0
- package/dist/useIsDesktop.js +23 -0
- package/dist/useIsFirstRender.d.ts +12 -0
- package/dist/useIsFirstRender.js +21 -0
- package/dist/useList.d.ts +19 -0
- package/dist/useList.js +44 -0
- package/dist/useLocalNotifications.d.ts +23 -0
- package/dist/useLocalNotifications.js +50 -0
- package/dist/useLocalStorage.d.ts +45 -0
- package/dist/useLocalStorage.js +71 -0
- package/dist/useNetworkInformation.d.ts +138 -0
- package/dist/useNetworkInformation.js +76 -0
- package/dist/useOnline.d.ts +17 -0
- package/dist/useOnline.js +29 -0
- package/dist/usePageVisibility.d.ts +32 -0
- package/dist/usePageVisibility.js +65 -0
- package/dist/usePermissions.d.ts +28 -0
- package/dist/usePermissions.js +70 -0
- package/dist/usePictureInPicture.d.ts +47 -0
- package/dist/usePictureInPicture.js +60 -0
- package/dist/usePopover.d.ts +54 -0
- package/dist/usePopover.js +67 -0
- package/dist/usePreferredLanguage.d.ts +55 -0
- package/dist/usePreferredLanguage.js +127 -0
- package/dist/usePreferredTheme.d.ts +67 -0
- package/dist/usePreferredTheme.js +133 -0
- package/dist/usePreviousDistinct.d.ts +12 -0
- package/dist/usePreviousDistinct.js +23 -0
- package/dist/useResettableState.d.ts +15 -0
- package/dist/useResettableState.js +25 -0
- package/dist/useScreenOrientation.d.ts +48 -0
- package/dist/useScreenOrientation.js +51 -0
- package/dist/useScreenSize.d.ts +16 -0
- package/dist/useScreenSize.js +34 -0
- package/dist/useScreenWakeLock.d.ts +37 -0
- package/dist/useScreenWakeLock.js +48 -0
- package/dist/useServerSentEvent.d.ts +57 -0
- package/dist/useServerSentEvent.js +78 -0
- package/dist/useShoppingCart.d.ts +54 -0
- package/dist/useShoppingCart.js +122 -0
- package/dist/useSmartVideo.d.ts +35 -0
- package/dist/useSmartVideo.js +76 -0
- package/dist/useSpeech.d.ts +74 -0
- package/dist/useSpeech.js +156 -0
- package/dist/useSummarizer.d.ts +92 -0
- package/dist/useSummarizer.js +83 -0
- package/dist/useTaskQueue.d.ts +25 -0
- package/dist/useTaskQueue.js +51 -0
- package/dist/useThrottledCallback.d.ts +32 -0
- package/dist/useThrottledCallback.js +42 -0
- package/dist/useTimeout.d.ts +58 -0
- package/dist/useTimeout.js +70 -0
- package/dist/useToggle.d.ts +30 -0
- package/dist/useToggle.js +23 -0
- package/dist/useTraceUpdates.d.ts +22 -0
- package/dist/useTraceUpdates.js +38 -0
- package/dist/useTranslator.d.ts +110 -0
- package/dist/useTranslator.js +119 -0
- package/dist/useUserActivation.d.ts +40 -0
- package/dist/useUserActivation.js +63 -0
- package/dist/useVibration.d.ts +55 -0
- package/dist/useVibration.js +50 -0
- package/dist/useWebsocket.d.ts +80 -0
- package/dist/useWebsocket.js +125 -0
- package/package.json +70 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useCameraCapture.ts
|
|
4
|
+
function createExternalStore(getSnapshot) {
|
|
5
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
6
|
+
return {
|
|
7
|
+
getSnapshot,
|
|
8
|
+
notify() {
|
|
9
|
+
listeners.forEach((l) => l());
|
|
10
|
+
},
|
|
11
|
+
subscribe(listener) {
|
|
12
|
+
listeners.add(listener);
|
|
13
|
+
return () => listeners.delete(listener);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Unopinionated, SSR-safe React hook for capturing still photos from the user's camera using getUserMedia and Canvas.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* const { videoRef, requestPermission, capture } = useCameraCapture();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function useCameraCapture(options = {}) {
|
|
26
|
+
const { onCapture, width = 320 } = options;
|
|
27
|
+
const output = {
|
|
28
|
+
quality: options.format?.quality ?? .8,
|
|
29
|
+
type: options.format?.type ?? "image/png"
|
|
30
|
+
};
|
|
31
|
+
const videoRef = React.useRef(null);
|
|
32
|
+
const canvasRef = React.useRef(null);
|
|
33
|
+
const imageRef = React.useRef(null);
|
|
34
|
+
const streamRef = React.useRef(null);
|
|
35
|
+
const streamingRef = React.useRef(false);
|
|
36
|
+
const heightRef = React.useRef(0);
|
|
37
|
+
const isBrowser = typeof window !== "undefined";
|
|
38
|
+
const permissionStoreRef = React.useRef(createExternalStore(() => !!streamRef.current));
|
|
39
|
+
const streamingStoreRef = React.useRef(createExternalStore(() => streamingRef.current));
|
|
40
|
+
const requestPermission = React.useCallback(async () => {
|
|
41
|
+
if (!isBrowser) return false;
|
|
42
|
+
if (!navigator.mediaDevices?.getUserMedia) return false;
|
|
43
|
+
try {
|
|
44
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
45
|
+
audio: false,
|
|
46
|
+
video: true
|
|
47
|
+
});
|
|
48
|
+
streamRef.current = stream;
|
|
49
|
+
permissionStoreRef.current.notify();
|
|
50
|
+
const video = videoRef.current;
|
|
51
|
+
if (!video) return false;
|
|
52
|
+
video.srcObject = stream;
|
|
53
|
+
await video.play();
|
|
54
|
+
if (!streamingRef.current) {
|
|
55
|
+
const { videoHeight, videoWidth } = video;
|
|
56
|
+
heightRef.current = videoHeight / (videoWidth / width);
|
|
57
|
+
video.width = width;
|
|
58
|
+
video.height = heightRef.current;
|
|
59
|
+
const canvas = canvasRef.current;
|
|
60
|
+
if (canvas) {
|
|
61
|
+
canvas.width = width;
|
|
62
|
+
canvas.height = heightRef.current;
|
|
63
|
+
}
|
|
64
|
+
streamingRef.current = true;
|
|
65
|
+
streamingStoreRef.current.notify();
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}, [isBrowser, width]);
|
|
72
|
+
const capture = React.useCallback(() => {
|
|
73
|
+
const video = videoRef.current;
|
|
74
|
+
const canvas = canvasRef.current;
|
|
75
|
+
const image = imageRef.current;
|
|
76
|
+
if (!video || !canvas || !streamingRef.current) return null;
|
|
77
|
+
const context = canvas.getContext("2d");
|
|
78
|
+
if (!context) return null;
|
|
79
|
+
canvas.width = width;
|
|
80
|
+
canvas.height = heightRef.current;
|
|
81
|
+
context.drawImage(video, 0, 0, width, heightRef.current);
|
|
82
|
+
const dataUrl = canvas.toDataURL(output.type, output.quality);
|
|
83
|
+
image?.setAttribute("src", dataUrl);
|
|
84
|
+
if (onCapture) canvas.toBlob((blob) => {
|
|
85
|
+
if (blob) onCapture(dataUrl, blob);
|
|
86
|
+
}, output.type, output.quality);
|
|
87
|
+
return dataUrl;
|
|
88
|
+
}, [
|
|
89
|
+
onCapture,
|
|
90
|
+
width,
|
|
91
|
+
output.quality,
|
|
92
|
+
output.type
|
|
93
|
+
]);
|
|
94
|
+
const stop = React.useCallback(() => {
|
|
95
|
+
streamRef.current?.getTracks().forEach((track) => track.stop());
|
|
96
|
+
streamRef.current = null;
|
|
97
|
+
streamingRef.current = false;
|
|
98
|
+
permissionStoreRef.current.notify();
|
|
99
|
+
streamingStoreRef.current.notify();
|
|
100
|
+
}, []);
|
|
101
|
+
const usePermission = () => React.useSyncExternalStore(permissionStoreRef.current.subscribe, permissionStoreRef.current.getSnapshot, () => false);
|
|
102
|
+
const useStreaming = () => React.useSyncExternalStore(streamingStoreRef.current.subscribe, streamingStoreRef.current.getSnapshot, () => false);
|
|
103
|
+
return {
|
|
104
|
+
canvasRef,
|
|
105
|
+
capture,
|
|
106
|
+
imageRef,
|
|
107
|
+
requestPermission,
|
|
108
|
+
stop,
|
|
109
|
+
usePermission,
|
|
110
|
+
useStreaming,
|
|
111
|
+
videoRef
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
export { useCameraCapture };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/useCookies.d.ts
|
|
2
|
+
interface UseCookieOptions {
|
|
3
|
+
domain?: string;
|
|
4
|
+
expires?: Date;
|
|
5
|
+
maxAge?: number;
|
|
6
|
+
path?: string;
|
|
7
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
8
|
+
secure?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface UseCookieReturn {
|
|
11
|
+
/**
|
|
12
|
+
* Reads a cookie value.
|
|
13
|
+
*/
|
|
14
|
+
get: (name: string) => string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Reads all cookies as a key-value map.
|
|
17
|
+
*/
|
|
18
|
+
getAll: () => Record<string, string>;
|
|
19
|
+
/**
|
|
20
|
+
* Removes a cookie.
|
|
21
|
+
*/
|
|
22
|
+
remove: (name: string, options?: UseCookieOptions) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Sets a cookie value.
|
|
25
|
+
*/
|
|
26
|
+
set: (name: string, value: string, options?: UseCookieOptions) => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `useCookies` is a React hook that provides unopinionated access to document cookies.
|
|
30
|
+
* This hook does not sync cookies to React state and does not perform encoding beyond basic string handling.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* const cookies = useCookies();
|
|
35
|
+
* cookies.set('token', 'abc', { secure: true });
|
|
36
|
+
* const token = cookies.get('token');
|
|
37
|
+
* ```
|
|
38
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Cookie_Store_API
|
|
39
|
+
*/
|
|
40
|
+
declare function useCookies(): UseCookieReturn;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { useCookies };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useCookies.ts
|
|
4
|
+
/**
|
|
5
|
+
* `useCookies` is a React hook that provides unopinionated access to document cookies.
|
|
6
|
+
* This hook does not sync cookies to React state and does not perform encoding beyond basic string handling.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const cookies = useCookies();
|
|
11
|
+
* cookies.set('token', 'abc', { secure: true });
|
|
12
|
+
* const token = cookies.get('token');
|
|
13
|
+
* ```
|
|
14
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Cookie_Store_API
|
|
15
|
+
*/
|
|
16
|
+
function useCookies() {
|
|
17
|
+
const isSupported = typeof document !== "undefined" && typeof document.cookie !== "undefined";
|
|
18
|
+
const get = React.useCallback((name) => {
|
|
19
|
+
if (!isSupported) return null;
|
|
20
|
+
const cookies = document.cookie.split("; ");
|
|
21
|
+
for (const cookie of cookies) {
|
|
22
|
+
const [key, ...rest] = cookie.split("=");
|
|
23
|
+
if (key === name) return decodeURIComponent(rest.join("="));
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}, [isSupported]);
|
|
27
|
+
const set = React.useCallback((name, value, options = {}) => {
|
|
28
|
+
if (!isSupported) return;
|
|
29
|
+
let cookie = `${name}=${encodeURIComponent(value)}`;
|
|
30
|
+
if (options.maxAge !== void 0) cookie += `; max-age=${options.maxAge}`;
|
|
31
|
+
if (options.expires !== void 0) cookie += `; expires=${options.expires.toUTCString()}`;
|
|
32
|
+
if (options.path) cookie += `; path=${options.path}`;
|
|
33
|
+
if (options.domain) cookie += `; domain=${options.domain}`;
|
|
34
|
+
if (options.secure) cookie += "; secure";
|
|
35
|
+
if (options.sameSite) cookie += `; samesite=${options.sameSite}`;
|
|
36
|
+
document.cookie = cookie;
|
|
37
|
+
}, [isSupported]);
|
|
38
|
+
const remove = React.useCallback((name, options = {}) => {
|
|
39
|
+
set(name, "", {
|
|
40
|
+
...options,
|
|
41
|
+
maxAge: 0
|
|
42
|
+
});
|
|
43
|
+
}, [set]);
|
|
44
|
+
return {
|
|
45
|
+
get,
|
|
46
|
+
getAll: React.useCallback(() => {
|
|
47
|
+
if (!isSupported) return {};
|
|
48
|
+
return document.cookie.split("; ").filter(Boolean).reduce((acc, cookie) => {
|
|
49
|
+
const [key, ...rest] = cookie.split("=");
|
|
50
|
+
if (!key) return acc;
|
|
51
|
+
acc[key] = decodeURIComponent(rest.join("="));
|
|
52
|
+
return acc;
|
|
53
|
+
}, {});
|
|
54
|
+
}, [isSupported]),
|
|
55
|
+
remove,
|
|
56
|
+
set
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { useCookies };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/useCopyToClipboard.d.ts
|
|
2
|
+
interface CopyToClipboardReturn {
|
|
3
|
+
copyToClipboard: (text: string) => Promise<void>;
|
|
4
|
+
textCopied: string | null;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `useCopyToClipboard` provides a safe way to copy text to the clipboard
|
|
8
|
+
* using the Clipboard API.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { textCopied, copyToClipboard } = useCopyToClipboard();
|
|
13
|
+
*
|
|
14
|
+
* <button onClick={() => copyToClipboard('Hello!')}>
|
|
15
|
+
* Copy
|
|
16
|
+
* </button>
|
|
17
|
+
* ```
|
|
18
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API
|
|
19
|
+
*/
|
|
20
|
+
declare function useCopyToClipboard(): CopyToClipboardReturn;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { useCopyToClipboard };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useCopyToClipboard.ts
|
|
4
|
+
/**
|
|
5
|
+
* `useCopyToClipboard` provides a safe way to copy text to the clipboard
|
|
6
|
+
* using the Clipboard API.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const { textCopied, copyToClipboard } = useCopyToClipboard();
|
|
11
|
+
*
|
|
12
|
+
* <button onClick={() => copyToClipboard('Hello!')}>
|
|
13
|
+
* Copy
|
|
14
|
+
* </button>
|
|
15
|
+
* ```
|
|
16
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API
|
|
17
|
+
*/
|
|
18
|
+
function useCopyToClipboard() {
|
|
19
|
+
const [textCopied, setTextCopied] = React.useState(null);
|
|
20
|
+
return {
|
|
21
|
+
copyToClipboard: React.useCallback(async (text) => {
|
|
22
|
+
if (!navigator?.clipboard?.writeText) throw new Error("Clipboard API not available.");
|
|
23
|
+
await navigator.clipboard.writeText(text);
|
|
24
|
+
setTextCopied(text);
|
|
25
|
+
}, []),
|
|
26
|
+
textCopied
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { useCopyToClipboard };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/useCountDown.d.ts
|
|
2
|
+
type CountDownStatus = 'start' | 'running' | 'paused' | 'completed';
|
|
3
|
+
interface CountDownOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The interval en milliseconds between each tick.
|
|
6
|
+
*/
|
|
7
|
+
interval?: number;
|
|
8
|
+
/**
|
|
9
|
+
* A callback to be called when the countdown ends.
|
|
10
|
+
*/
|
|
11
|
+
onComplete?: () => void;
|
|
12
|
+
/**
|
|
13
|
+
* A callback to be called on each tick.
|
|
14
|
+
*/
|
|
15
|
+
onTick?: (time: number) => void;
|
|
16
|
+
}
|
|
17
|
+
interface CountDown {
|
|
18
|
+
/**
|
|
19
|
+
* The end time of the countdown in milliseconds.
|
|
20
|
+
*/
|
|
21
|
+
endTime: number;
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
options?: CountDownOptions;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to start the countdown on mount. Default: false.
|
|
28
|
+
*/
|
|
29
|
+
startOnMount?: boolean;
|
|
30
|
+
}
|
|
31
|
+
interface CountDownControlls {
|
|
32
|
+
/**
|
|
33
|
+
* Increments the countdown by a given amount of time in milliseconds.
|
|
34
|
+
*/
|
|
35
|
+
increment: (ms?: number) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Pauses the countdown.
|
|
38
|
+
*/
|
|
39
|
+
pause: () => void;
|
|
40
|
+
/**
|
|
41
|
+
* Resets the countdown.
|
|
42
|
+
*/
|
|
43
|
+
reset: () => void;
|
|
44
|
+
/**
|
|
45
|
+
* Resumes the countdown
|
|
46
|
+
*/
|
|
47
|
+
resume: () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Starts the countdown.
|
|
50
|
+
*/
|
|
51
|
+
start: () => void;
|
|
52
|
+
}
|
|
53
|
+
interface CountDownReturn {
|
|
54
|
+
/**
|
|
55
|
+
* Stable references to control functions.
|
|
56
|
+
*/
|
|
57
|
+
controls: CountDownControlls;
|
|
58
|
+
/**
|
|
59
|
+
* Remaining time in milliseconds.
|
|
60
|
+
* Will be `0` when the countdown has completed.
|
|
61
|
+
*/
|
|
62
|
+
count: number | null;
|
|
63
|
+
/**
|
|
64
|
+
* Indicates the status of the countdown.
|
|
65
|
+
*/
|
|
66
|
+
status: CountDownStatus;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `useCountDown` is a controllable countdown hook based on an absolute end timestamp.
|
|
70
|
+
* It uses a reference timestamp approach to prevent time drift commonly association with simple `setInterval` forcing.
|
|
71
|
+
*
|
|
72
|
+
* @see https://developer.mozilla.org/es/docs/Web/API/Window/setInterval
|
|
73
|
+
*/
|
|
74
|
+
declare function useCountDown({
|
|
75
|
+
endTime,
|
|
76
|
+
options,
|
|
77
|
+
startOnMount
|
|
78
|
+
}: CountDown): CountDownReturn;
|
|
79
|
+
//#endregion
|
|
80
|
+
export { useCountDown };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useCountDown.ts
|
|
4
|
+
/**
|
|
5
|
+
* `useCountDown` is a controllable countdown hook based on an absolute end timestamp.
|
|
6
|
+
* It uses a reference timestamp approach to prevent time drift commonly association with simple `setInterval` forcing.
|
|
7
|
+
*
|
|
8
|
+
* @see https://developer.mozilla.org/es/docs/Web/API/Window/setInterval
|
|
9
|
+
*/
|
|
10
|
+
function useCountDown({ endTime, options, startOnMount = false }) {
|
|
11
|
+
const intervalValue = options?.interval ?? 1e3;
|
|
12
|
+
const initialDurationRef = React.useRef(endTime - Date.now());
|
|
13
|
+
const endTimeRef = React.useRef(startOnMount ? Date.now() + initialDurationRef.current : 0);
|
|
14
|
+
const completedRef = React.useRef(false);
|
|
15
|
+
const remainingAtPauseRef = React.useRef(initialDurationRef.current);
|
|
16
|
+
const intervalIdRef = React.useRef(null);
|
|
17
|
+
const optionsRef = React.useRef(options);
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
optionsRef.current = options;
|
|
20
|
+
}, [options]);
|
|
21
|
+
const [count, setCount] = React.useState(initialDurationRef.current);
|
|
22
|
+
const [status, setStatus] = React.useState(startOnMount ? "running" : "start");
|
|
23
|
+
const clearTimer = React.useCallback(() => {
|
|
24
|
+
if (intervalIdRef.current !== null) {
|
|
25
|
+
clearInterval(intervalIdRef.current);
|
|
26
|
+
intervalIdRef.current = null;
|
|
27
|
+
}
|
|
28
|
+
}, []);
|
|
29
|
+
const tick = React.useCallback(() => {
|
|
30
|
+
const remaining = Math.max(endTimeRef.current - Date.now(), 0);
|
|
31
|
+
optionsRef.current?.onTick?.(remaining);
|
|
32
|
+
setCount(remaining);
|
|
33
|
+
if (remaining <= 0 && !completedRef.current) {
|
|
34
|
+
completedRef.current = true;
|
|
35
|
+
clearTimer();
|
|
36
|
+
setStatus("completed");
|
|
37
|
+
optionsRef.current?.onComplete?.();
|
|
38
|
+
}
|
|
39
|
+
}, [clearTimer]);
|
|
40
|
+
const pause = React.useCallback(() => {
|
|
41
|
+
if (status !== "running") return;
|
|
42
|
+
clearTimer();
|
|
43
|
+
remainingAtPauseRef.current = Math.max(endTimeRef.current - Date.now(), 0);
|
|
44
|
+
setStatus("paused");
|
|
45
|
+
}, [status, clearTimer]);
|
|
46
|
+
const resume = React.useCallback(() => {
|
|
47
|
+
if (status !== "paused") return;
|
|
48
|
+
endTimeRef.current = Date.now() + remainingAtPauseRef.current;
|
|
49
|
+
setStatus("running");
|
|
50
|
+
}, [status]);
|
|
51
|
+
const reset = React.useCallback(() => {
|
|
52
|
+
clearTimer();
|
|
53
|
+
completedRef.current = false;
|
|
54
|
+
remainingAtPauseRef.current = initialDurationRef.current;
|
|
55
|
+
setCount(initialDurationRef.current);
|
|
56
|
+
setStatus("start");
|
|
57
|
+
}, [clearTimer]);
|
|
58
|
+
const increment = React.useCallback((ms) => {
|
|
59
|
+
if (!ms || ms <= 0 || completedRef.current) return;
|
|
60
|
+
endTimeRef.current += ms;
|
|
61
|
+
if (status === "paused") {
|
|
62
|
+
remainingAtPauseRef.current += ms;
|
|
63
|
+
setCount(remainingAtPauseRef.current);
|
|
64
|
+
}
|
|
65
|
+
}, [status]);
|
|
66
|
+
const start = React.useCallback(() => {
|
|
67
|
+
if (status !== "start") return;
|
|
68
|
+
endTimeRef.current = Date.now() + remainingAtPauseRef.current;
|
|
69
|
+
setStatus("running");
|
|
70
|
+
}, [status]);
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
if (status !== "running") {
|
|
73
|
+
clearTimer();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
completedRef.current = false;
|
|
77
|
+
tick();
|
|
78
|
+
intervalIdRef.current = setInterval(tick, intervalValue);
|
|
79
|
+
return clearTimer;
|
|
80
|
+
}, [
|
|
81
|
+
status,
|
|
82
|
+
intervalValue,
|
|
83
|
+
tick,
|
|
84
|
+
clearTimer
|
|
85
|
+
]);
|
|
86
|
+
return {
|
|
87
|
+
controls: React.useMemo(() => ({
|
|
88
|
+
increment,
|
|
89
|
+
pause,
|
|
90
|
+
reset,
|
|
91
|
+
resume,
|
|
92
|
+
start
|
|
93
|
+
}), [
|
|
94
|
+
start,
|
|
95
|
+
pause,
|
|
96
|
+
resume,
|
|
97
|
+
reset,
|
|
98
|
+
increment
|
|
99
|
+
]),
|
|
100
|
+
count,
|
|
101
|
+
status
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
export { useCountDown };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useDebouncedState.d.ts
|
|
4
|
+
interface UseDebouncedStateOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Delay in milliseconds before the debounced value is updated.
|
|
7
|
+
*/
|
|
8
|
+
delay: number;
|
|
9
|
+
}
|
|
10
|
+
interface UseDebouncedStateReturn<T> {
|
|
11
|
+
/**
|
|
12
|
+
* The debounced value, updated after the specified delay.
|
|
13
|
+
*/
|
|
14
|
+
debouncedValue: T;
|
|
15
|
+
/**
|
|
16
|
+
* State setter for the inmediate value.
|
|
17
|
+
*/
|
|
18
|
+
setValue: React.Dispatch<React.SetStateAction<T>>;
|
|
19
|
+
/**
|
|
20
|
+
* The inmediate (non-debounced) value.
|
|
21
|
+
*/
|
|
22
|
+
value: T;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `useDebouncedState` is a React hook that manages a state value and exposes a debounced version of it.
|
|
26
|
+
* The debounced value is updated only after the specified delay has elapsed since the last change to the inmediate value.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* const { value, debouncedValue, setValue } = useDebouncedState('', {
|
|
31
|
+
* delay: 400,
|
|
32
|
+
* });
|
|
33
|
+
* React.useEffect(() => {
|
|
34
|
+
* if (!debouncedValue) return;
|
|
35
|
+
* search(debouncedValue);
|
|
36
|
+
* }, [debouncedValue]);
|
|
37
|
+
* return (
|
|
38
|
+
* <input
|
|
39
|
+
* value={value}
|
|
40
|
+
* onChange={(e) => setValue(e.target.value)}
|
|
41
|
+
* />
|
|
42
|
+
* );
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
declare function useDebouncedState<T>(initialValue: T, options: UseDebouncedStateOptions): UseDebouncedStateReturn<T>;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { useDebouncedState };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useDebouncedState.ts
|
|
4
|
+
/**
|
|
5
|
+
* `useDebouncedState` is a React hook that manages a state value and exposes a debounced version of it.
|
|
6
|
+
* The debounced value is updated only after the specified delay has elapsed since the last change to the inmediate value.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const { value, debouncedValue, setValue } = useDebouncedState('', {
|
|
11
|
+
* delay: 400,
|
|
12
|
+
* });
|
|
13
|
+
* React.useEffect(() => {
|
|
14
|
+
* if (!debouncedValue) return;
|
|
15
|
+
* search(debouncedValue);
|
|
16
|
+
* }, [debouncedValue]);
|
|
17
|
+
* return (
|
|
18
|
+
* <input
|
|
19
|
+
* value={value}
|
|
20
|
+
* onChange={(e) => setValue(e.target.value)}
|
|
21
|
+
* />
|
|
22
|
+
* );
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function useDebouncedState(initialValue, options) {
|
|
26
|
+
const { delay } = options;
|
|
27
|
+
const [value, setValue] = React.useState(initialValue);
|
|
28
|
+
const [debouncedValue, setDebouncedValue] = React.useState(initialValue);
|
|
29
|
+
const timeoutRef = React.useRef(null);
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
32
|
+
timeoutRef.current = setTimeout(() => {
|
|
33
|
+
setDebouncedValue(value);
|
|
34
|
+
}, delay);
|
|
35
|
+
return () => {
|
|
36
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
37
|
+
};
|
|
38
|
+
}, [value, delay]);
|
|
39
|
+
return {
|
|
40
|
+
debouncedValue,
|
|
41
|
+
setValue,
|
|
42
|
+
value
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { useDebouncedState };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/useExternalNotifications.d.ts
|
|
2
|
+
interface NotificationPayload extends NotificationOptions {
|
|
3
|
+
createdAt: number;
|
|
4
|
+
id?: string;
|
|
5
|
+
title: string;
|
|
6
|
+
}
|
|
7
|
+
interface UseExternalNotificationReturn {
|
|
8
|
+
isSupported: boolean;
|
|
9
|
+
notifications: NotificationPayload[] | null;
|
|
10
|
+
notify: (notification: Omit<NotificationPayload, 'createdAt'>) => void;
|
|
11
|
+
permission: NotificationPermission;
|
|
12
|
+
requestPermission: () => Promise<NotificationPermission>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* `useExternalNotifications` is React hook for consuming and emitting external notifications using a global store.
|
|
16
|
+
* It is thinking for events outside the normal cycle of React:
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* function NotificationCenter() {
|
|
21
|
+
* const { notifications } = useExternalNotification();
|
|
22
|
+
*
|
|
23
|
+
* return (
|
|
24
|
+
* <ul>
|
|
25
|
+
* {notifications.map(n => (
|
|
26
|
+
* <li key={n.id}>{n.title}</li>
|
|
27
|
+
* ))}
|
|
28
|
+
* </ul>
|
|
29
|
+
* );
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Notification
|
|
33
|
+
*/
|
|
34
|
+
declare function useExternalNotifications(): UseExternalNotificationReturn;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { useExternalNotifications };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/useExternalNotifications.ts
|
|
4
|
+
let notifications = [];
|
|
5
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
6
|
+
let isListening = false;
|
|
7
|
+
function emit() {
|
|
8
|
+
listeners.forEach((listener) => listener());
|
|
9
|
+
}
|
|
10
|
+
function onStorageEvent(event) {
|
|
11
|
+
if (event.key !== "notifications") return;
|
|
12
|
+
try {
|
|
13
|
+
const next = JSON.parse(event.newValue ?? "[]");
|
|
14
|
+
if (Object.is(next, notifications)) return;
|
|
15
|
+
notifications = next;
|
|
16
|
+
emit();
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
function suscribe(listener) {
|
|
20
|
+
listeners.add(listener);
|
|
21
|
+
if (!isListening && typeof window !== "undefined") {
|
|
22
|
+
window.addEventListener("storage", onStorageEvent);
|
|
23
|
+
isListening = true;
|
|
24
|
+
}
|
|
25
|
+
return () => {
|
|
26
|
+
listeners.delete(listener);
|
|
27
|
+
if (listeners.size === 0 && isListening) {
|
|
28
|
+
window.removeEventListener("storage", onStorageEvent);
|
|
29
|
+
isListening = false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function getSnapshot() {
|
|
34
|
+
return notifications;
|
|
35
|
+
}
|
|
36
|
+
const notificationStore = {
|
|
37
|
+
getSnapshot,
|
|
38
|
+
push(notification) {
|
|
39
|
+
notifications = [...notifications, notification];
|
|
40
|
+
localStorage.setItem("notifications", JSON.stringify(notifications));
|
|
41
|
+
emit();
|
|
42
|
+
},
|
|
43
|
+
suscribe
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* `useExternalNotifications` is React hook for consuming and emitting external notifications using a global store.
|
|
47
|
+
* It is thinking for events outside the normal cycle of React:
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* function NotificationCenter() {
|
|
52
|
+
* const { notifications } = useExternalNotification();
|
|
53
|
+
*
|
|
54
|
+
* return (
|
|
55
|
+
* <ul>
|
|
56
|
+
* {notifications.map(n => (
|
|
57
|
+
* <li key={n.id}>{n.title}</li>
|
|
58
|
+
* ))}
|
|
59
|
+
* </ul>
|
|
60
|
+
* );
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Notification
|
|
64
|
+
*/
|
|
65
|
+
function useExternalNotifications() {
|
|
66
|
+
const notifications$1 = React.useSyncExternalStore(notificationStore.suscribe, notificationStore.getSnapshot, () => null);
|
|
67
|
+
const isSupported = typeof window !== "undefined" && "Notification" in window;
|
|
68
|
+
const permission = isSupported ? Notification.permission : "denied";
|
|
69
|
+
const requestPermission = React.useCallback(async () => {
|
|
70
|
+
if (!isSupported) return "denied";
|
|
71
|
+
return await Notification.requestPermission();
|
|
72
|
+
}, [isSupported]);
|
|
73
|
+
return {
|
|
74
|
+
isSupported,
|
|
75
|
+
notifications: notifications$1,
|
|
76
|
+
notify: React.useCallback((notification) => {
|
|
77
|
+
const payload = {
|
|
78
|
+
...notification,
|
|
79
|
+
createdAt: Date.now()
|
|
80
|
+
};
|
|
81
|
+
notificationStore.push(payload);
|
|
82
|
+
if (!isSupported || permission !== "granted") return;
|
|
83
|
+
const options = {
|
|
84
|
+
...notification.badge !== void 0 && { badge: notification.badge },
|
|
85
|
+
...notification.body !== void 0 && { body: notification.body },
|
|
86
|
+
...notification.data !== void 0 && { data: notification.data },
|
|
87
|
+
...notification.dir !== void 0 && { dir: notification.dir },
|
|
88
|
+
...notification.icon !== void 0 && { icon: notification.icon },
|
|
89
|
+
...notification.lang !== void 0 && { lang: notification.lang },
|
|
90
|
+
...notification.requireInteraction !== void 0 && { requireInteraction: notification.requireInteraction }
|
|
91
|
+
};
|
|
92
|
+
new Notification(payload.title, options);
|
|
93
|
+
}, [isSupported, permission]),
|
|
94
|
+
permission,
|
|
95
|
+
requestPermission
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
export { useExternalNotifications };
|