@studiocubics/hooks 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/CHANGELOG.md +9 -0
- package/README.md +73 -0
- package/eslint.config.js +21 -0
- package/package.json +59 -0
- package/rollup.config.js +28 -0
- package/src/index.ts +11 -0
- package/src/useAnchorElement/useAnchorElement.tsx +15 -0
- package/src/useDelayedAction/useDelayedAction.tsx +12 -0
- package/src/useDisclosure/useDisclosure.tsx +36 -0
- package/src/useEventCallback/useEventCallback.tsx +26 -0
- package/src/useEventListener/useEventListener.tsx +92 -0
- package/src/useFormHelpers/useFormHelpers.tsx +29 -0
- package/src/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.tsx +6 -0
- package/src/useLocalStorage/useLocalStorage.tsx +165 -0
- package/src/useMounted/useMounted.tsx +11 -0
- package/src/useMousePosition/useMousePosition.tsx +60 -0
- package/src/useScreenSize/useScreenSize.tsx +74 -0
- package/tsconfig.json +31 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
9
|
+
|
|
10
|
+
## React Compiler
|
|
11
|
+
|
|
12
|
+
The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress.
|
|
13
|
+
|
|
14
|
+
## Expanding the ESLint configuration
|
|
15
|
+
|
|
16
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
export default defineConfig([
|
|
20
|
+
globalIgnores(['dist']),
|
|
21
|
+
{
|
|
22
|
+
files: ['**/*.{ts,tsx}'],
|
|
23
|
+
extends: [
|
|
24
|
+
// Other configs...
|
|
25
|
+
|
|
26
|
+
// Remove tseslint.configs.recommended and replace with this
|
|
27
|
+
tseslint.configs.recommendedTypeChecked,
|
|
28
|
+
// Alternatively, use this for stricter rules
|
|
29
|
+
tseslint.configs.strictTypeChecked,
|
|
30
|
+
// Optionally, add this for stylistic rules
|
|
31
|
+
tseslint.configs.stylisticTypeChecked,
|
|
32
|
+
|
|
33
|
+
// Other configs...
|
|
34
|
+
],
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parserOptions: {
|
|
37
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
38
|
+
tsconfigRootDir: import.meta.dirname,
|
|
39
|
+
},
|
|
40
|
+
// other options...
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
// eslint.config.js
|
|
50
|
+
import reactX from 'eslint-plugin-react-x'
|
|
51
|
+
import reactDom from 'eslint-plugin-react-dom'
|
|
52
|
+
|
|
53
|
+
export default defineConfig([
|
|
54
|
+
globalIgnores(['dist']),
|
|
55
|
+
{
|
|
56
|
+
files: ['**/*.{ts,tsx}'],
|
|
57
|
+
extends: [
|
|
58
|
+
// Other configs...
|
|
59
|
+
// Enable lint rules for React
|
|
60
|
+
reactX.configs['recommended-typescript'],
|
|
61
|
+
// Enable lint rules for React DOM
|
|
62
|
+
reactDom.configs.recommended,
|
|
63
|
+
],
|
|
64
|
+
languageOptions: {
|
|
65
|
+
parserOptions: {
|
|
66
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
67
|
+
tsconfigRootDir: import.meta.dirname,
|
|
68
|
+
},
|
|
69
|
+
// other options...
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
])
|
|
73
|
+
```
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
4
|
+
import tseslint from "typescript-eslint";
|
|
5
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
6
|
+
|
|
7
|
+
export default defineConfig([
|
|
8
|
+
globalIgnores(["dist"]),
|
|
9
|
+
{
|
|
10
|
+
files: ["**/*.{ts,tsx}"],
|
|
11
|
+
extends: [
|
|
12
|
+
js.configs.recommended,
|
|
13
|
+
tseslint.configs.recommended,
|
|
14
|
+
reactHooks.configs.flat.recommended,
|
|
15
|
+
],
|
|
16
|
+
languageOptions: {
|
|
17
|
+
ecmaVersion: 2020,
|
|
18
|
+
globals: globals.browser,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
]);
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@studiocubics/hooks",
|
|
3
|
+
"description": "Package containing important hooks by Studio Cubics",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"private": false,
|
|
8
|
+
"version": "0.0.1",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"@studiocubics",
|
|
11
|
+
"cubics",
|
|
12
|
+
"studio",
|
|
13
|
+
"react",
|
|
14
|
+
"hooks"
|
|
15
|
+
],
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Studio Cubics",
|
|
18
|
+
"email": "studiocubics7@gmail.com",
|
|
19
|
+
"url": "https://studio-cubics.vercel.app"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./dist/index.js",
|
|
26
|
+
"./styles.css": "./dist/index.css"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": "^19.2.0",
|
|
30
|
+
"react-dom": "^19.2.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@studiocubics/types": "^0.0.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@eslint/js": "^9.39.1",
|
|
37
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
38
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
39
|
+
"@types/node": "^24.10.1",
|
|
40
|
+
"@types/react": "^19.2.5",
|
|
41
|
+
"@types/react-dom": "^19.2.3",
|
|
42
|
+
"eslint": "^9.39.1",
|
|
43
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
44
|
+
"globals": "^16.5.0",
|
|
45
|
+
"postcss": "^8.5.6",
|
|
46
|
+
"postcss-modules": "^6.0.1",
|
|
47
|
+
"rollup": "^4.53.3",
|
|
48
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
49
|
+
"rollup-preserve-directives": "^1.1.3",
|
|
50
|
+
"typescript": "~5.9.3",
|
|
51
|
+
"typescript-eslint": "^8.46.4"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "rollup -c",
|
|
55
|
+
"clean:build": "rimraf dist node_modules && rollup -c",
|
|
56
|
+
"lint": "eslint .",
|
|
57
|
+
"clean": "rimraf dist node_modules"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineConfig } from "rollup";
|
|
2
|
+
import typescript from "@rollup/plugin-typescript";
|
|
3
|
+
import postcss from "rollup-plugin-postcss";
|
|
4
|
+
import terser from "@rollup/plugin-terser";
|
|
5
|
+
import preserveDirectives from "rollup-preserve-directives";
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
input: "src/index.ts",
|
|
9
|
+
output: {
|
|
10
|
+
preserveModules: true,
|
|
11
|
+
preserveModulesRoot: "src",
|
|
12
|
+
dir: "dist",
|
|
13
|
+
format: "esm",
|
|
14
|
+
sourcemap: true,
|
|
15
|
+
},
|
|
16
|
+
plugins: [
|
|
17
|
+
preserveDirectives(),
|
|
18
|
+
postcss({
|
|
19
|
+
include: "**/*.module.css",
|
|
20
|
+
modules: true,
|
|
21
|
+
extract: true,
|
|
22
|
+
minimize: true,
|
|
23
|
+
}),
|
|
24
|
+
typescript({ tsconfig: "./tsconfig.json" }),
|
|
25
|
+
terser({ compress: { directives: false } }),
|
|
26
|
+
],
|
|
27
|
+
external: ["react/jsx-runtime", "react", "react-dom"],
|
|
28
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./useAnchorElement/useAnchorElement";
|
|
2
|
+
export * from "./useDelayedAction/useDelayedAction";
|
|
3
|
+
export * from "./useDisclosure/useDisclosure";
|
|
4
|
+
export * from "./useEventCallback/useEventCallback";
|
|
5
|
+
export * from "./useEventListener/useEventListener";
|
|
6
|
+
export * from "./useFormHelpers/useFormHelpers";
|
|
7
|
+
export * from "./useIsomorphicLayoutEffect/useIsomorphicLayoutEffect";
|
|
8
|
+
export * from "./useLocalStorage/useLocalStorage";
|
|
9
|
+
export * from "./useMounted/useMounted";
|
|
10
|
+
export * from "./useMousePosition/useMousePosition";
|
|
11
|
+
export * from "./useScreenSize/useScreenSize";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type MouseEvent, useMemo, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export function useAnchorElement<T extends HTMLElement>() {
|
|
6
|
+
const [anchorEl, setAnchorEl] = useState<T | null>(null);
|
|
7
|
+
const open = useMemo(() => Boolean(anchorEl), [anchorEl]);
|
|
8
|
+
const handleClick = (event: MouseEvent<T>) => {
|
|
9
|
+
setAnchorEl(event.currentTarget);
|
|
10
|
+
};
|
|
11
|
+
const handleClose = () => {
|
|
12
|
+
setAnchorEl(null);
|
|
13
|
+
};
|
|
14
|
+
return { open, anchorEl, handleClick, handleClose, setAnchorEl };
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
|
|
5
|
+
export function useDelayedAction() {
|
|
6
|
+
const delayedExecute = useCallback(async (fn: Function, ms: number) => {
|
|
7
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
fn();
|
|
9
|
+
}, []);
|
|
10
|
+
|
|
11
|
+
return delayedExecute;
|
|
12
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
export function useDisclosure(initialState: boolean = false) {
|
|
6
|
+
const [open, setOpen] = useState(initialState);
|
|
7
|
+
|
|
8
|
+
function handleOpen() {
|
|
9
|
+
setOpen(true);
|
|
10
|
+
}
|
|
11
|
+
function handleClose() {
|
|
12
|
+
setOpen(false);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hijacking the handleClose function to prevent the dialog from closing when the user clicks outside the dialog or presses the escape key.
|
|
17
|
+
* @param _ event not going to be used.
|
|
18
|
+
* @param reason The reason the dialog was closed.
|
|
19
|
+
*/
|
|
20
|
+
function handleStrictClose(_: {}, reason: "backdropClick" | "escapeKeyDown") {
|
|
21
|
+
if (reason === "backdropClick" || reason === "escapeKeyDown") return;
|
|
22
|
+
handleClose();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function handleToggle() {
|
|
26
|
+
setOpen((prev) => !prev);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
open,
|
|
31
|
+
handleClose,
|
|
32
|
+
handleStrictClose,
|
|
33
|
+
handleOpen,
|
|
34
|
+
handleToggle,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef } from "react";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect";
|
|
5
|
+
|
|
6
|
+
export function useEventCallback<Args extends unknown[], R>(
|
|
7
|
+
fn: (...args: Args) => R
|
|
8
|
+
): (...args: Args) => R;
|
|
9
|
+
export function useEventCallback<Args extends unknown[], R>(
|
|
10
|
+
fn: ((...args: Args) => R) | undefined
|
|
11
|
+
): ((...args: Args) => R) | undefined;
|
|
12
|
+
export function useEventCallback<Args extends unknown[], R>(
|
|
13
|
+
fn: ((...args: Args) => R) | undefined
|
|
14
|
+
): ((...args: Args) => R) | undefined {
|
|
15
|
+
const ref = useRef<typeof fn>(() => {
|
|
16
|
+
throw new Error("Cannot call an event handler while rendering.");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
useIsomorphicLayoutEffect(() => {
|
|
20
|
+
ref.current = fn;
|
|
21
|
+
}, [fn]);
|
|
22
|
+
|
|
23
|
+
return useCallback((...args: Args) => ref.current?.(...args), [ref]) as (
|
|
24
|
+
...args: Args
|
|
25
|
+
) => R;
|
|
26
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
import type { RefObject } from "react";
|
|
6
|
+
import { useIsomorphicLayoutEffect } from "../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect";
|
|
7
|
+
|
|
8
|
+
// MediaQueryList Event based useEventListener interface
|
|
9
|
+
function useEventListener<K extends keyof MediaQueryListEventMap>(
|
|
10
|
+
eventName: K,
|
|
11
|
+
handler: (event: MediaQueryListEventMap[K]) => void,
|
|
12
|
+
element: RefObject<MediaQueryList>,
|
|
13
|
+
options?: boolean | AddEventListenerOptions,
|
|
14
|
+
): void;
|
|
15
|
+
|
|
16
|
+
// Window Event based useEventListener interface
|
|
17
|
+
function useEventListener<K extends keyof WindowEventMap>(
|
|
18
|
+
eventName: K,
|
|
19
|
+
handler: (event: WindowEventMap[K]) => void,
|
|
20
|
+
element?: undefined,
|
|
21
|
+
options?: boolean | AddEventListenerOptions,
|
|
22
|
+
): void;
|
|
23
|
+
|
|
24
|
+
// Element Event based useEventListener interface
|
|
25
|
+
function useEventListener<
|
|
26
|
+
K extends keyof HTMLElementEventMap & keyof SVGElementEventMap,
|
|
27
|
+
T extends Element = K extends keyof HTMLElementEventMap
|
|
28
|
+
? HTMLDivElement
|
|
29
|
+
: SVGElement,
|
|
30
|
+
>(
|
|
31
|
+
eventName: K,
|
|
32
|
+
handler:
|
|
33
|
+
| ((event: HTMLElementEventMap[K]) => void)
|
|
34
|
+
| ((event: SVGElementEventMap[K]) => void),
|
|
35
|
+
element: RefObject<T>,
|
|
36
|
+
options?: boolean | AddEventListenerOptions,
|
|
37
|
+
): void;
|
|
38
|
+
|
|
39
|
+
// Document Event based useEventListener interface
|
|
40
|
+
function useEventListener<K extends keyof DocumentEventMap>(
|
|
41
|
+
eventName: K,
|
|
42
|
+
handler: (event: DocumentEventMap[K]) => void,
|
|
43
|
+
element: RefObject<Document>,
|
|
44
|
+
options?: boolean | AddEventListenerOptions,
|
|
45
|
+
): void;
|
|
46
|
+
|
|
47
|
+
function useEventListener<
|
|
48
|
+
KW extends keyof WindowEventMap,
|
|
49
|
+
KH extends keyof HTMLElementEventMap & keyof SVGElementEventMap,
|
|
50
|
+
KM extends keyof MediaQueryListEventMap,
|
|
51
|
+
T extends HTMLElement | SVGAElement | MediaQueryList = HTMLElement,
|
|
52
|
+
>(
|
|
53
|
+
eventName: KW | KH | KM,
|
|
54
|
+
handler: (
|
|
55
|
+
event:
|
|
56
|
+
| WindowEventMap[KW]
|
|
57
|
+
| HTMLElementEventMap[KH]
|
|
58
|
+
| SVGElementEventMap[KH]
|
|
59
|
+
| MediaQueryListEventMap[KM]
|
|
60
|
+
| Event,
|
|
61
|
+
) => void,
|
|
62
|
+
element?: RefObject<T>,
|
|
63
|
+
options?: boolean | AddEventListenerOptions,
|
|
64
|
+
) {
|
|
65
|
+
// Create a ref that stores handler
|
|
66
|
+
const savedHandler = useRef(handler);
|
|
67
|
+
|
|
68
|
+
useIsomorphicLayoutEffect(() => {
|
|
69
|
+
savedHandler.current = handler;
|
|
70
|
+
}, [handler]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
// Define the listening target
|
|
74
|
+
const targetElement: T | Window = element?.current ?? window;
|
|
75
|
+
|
|
76
|
+
if (!(targetElement && targetElement.addEventListener)) return;
|
|
77
|
+
|
|
78
|
+
// Create event listener that calls handler function stored in ref
|
|
79
|
+
const listener: typeof handler = (event) => {
|
|
80
|
+
savedHandler.current(event);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
targetElement.addEventListener(eventName, listener, options);
|
|
84
|
+
|
|
85
|
+
// Remove event listener on cleanup
|
|
86
|
+
return () => {
|
|
87
|
+
targetElement.removeEventListener(eventName, listener, options);
|
|
88
|
+
};
|
|
89
|
+
}, [eventName, element, options]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { useEventListener };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
export type FormHelpersProps = {
|
|
6
|
+
initialLoading?: boolean;
|
|
7
|
+
initialError?: string | unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function useFormHelpers(props: FormHelpersProps) {
|
|
11
|
+
const [loading, setLoading] = useState(props?.initialLoading ?? false);
|
|
12
|
+
const [error, setError] = useState(props?.initialError ?? "");
|
|
13
|
+
|
|
14
|
+
function handleLoading(l: boolean) {
|
|
15
|
+
setLoading(l);
|
|
16
|
+
}
|
|
17
|
+
function handleError(e: string | unknown) {
|
|
18
|
+
if (typeof e === "string") setError(e);
|
|
19
|
+
if (e instanceof Error) setError(e.message);
|
|
20
|
+
console.error(e);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
loading,
|
|
25
|
+
error,
|
|
26
|
+
handleLoading,
|
|
27
|
+
handleError,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import type { Dispatch, SetStateAction } from "react";
|
|
5
|
+
import { useEventCallback } from "../useEventCallback/useEventCallback";
|
|
6
|
+
import { useEventListener } from "../useEventListener/useEventListener";
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface WindowEventMap {
|
|
10
|
+
"local-storage": CustomEvent;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type UseLocalStorageOptions<T> = {
|
|
15
|
+
serializer?: (value: T) => string;
|
|
16
|
+
deserializer?: (value: string) => T;
|
|
17
|
+
initializeWithValue?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const IS_SERVER = typeof window === "undefined";
|
|
21
|
+
|
|
22
|
+
export function useLocalStorage<T>(
|
|
23
|
+
key: string,
|
|
24
|
+
initialValue: T | (() => T),
|
|
25
|
+
options: UseLocalStorageOptions<T> = {},
|
|
26
|
+
): [T, Dispatch<SetStateAction<T>>, () => void] {
|
|
27
|
+
const { initializeWithValue = true } = options;
|
|
28
|
+
|
|
29
|
+
const serializer = useCallback<(value: T) => string>(
|
|
30
|
+
(value) => {
|
|
31
|
+
if (options.serializer) {
|
|
32
|
+
return options.serializer(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return JSON.stringify(value);
|
|
36
|
+
},
|
|
37
|
+
[options],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const deserializer = useCallback<(value: string) => T>(
|
|
41
|
+
(value) => {
|
|
42
|
+
if (options.deserializer) {
|
|
43
|
+
return options.deserializer(value);
|
|
44
|
+
}
|
|
45
|
+
// Support 'undefined' as a value
|
|
46
|
+
if (value === "undefined") {
|
|
47
|
+
return undefined as unknown as T;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const defaultValue =
|
|
51
|
+
initialValue instanceof Function ? initialValue() : initialValue;
|
|
52
|
+
|
|
53
|
+
let parsed: unknown;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(value);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("Error parsing JSON:", error);
|
|
58
|
+
return defaultValue; // Return initialValue if parsing fails
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return parsed as T;
|
|
62
|
+
},
|
|
63
|
+
[options, initialValue],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Get from local storage then
|
|
67
|
+
// parse stored json or return initialValue
|
|
68
|
+
const readValue = useCallback((): T => {
|
|
69
|
+
const initialValueToUse =
|
|
70
|
+
initialValue instanceof Function ? initialValue() : initialValue;
|
|
71
|
+
|
|
72
|
+
// Prevent build error "window is undefined" but keep working
|
|
73
|
+
if (IS_SERVER) {
|
|
74
|
+
return initialValueToUse;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const raw = window.localStorage.getItem(key);
|
|
79
|
+
return raw ? deserializer(raw) : initialValueToUse;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.warn(`Error reading localStorage key “${key}”:`, error);
|
|
82
|
+
return initialValueToUse;
|
|
83
|
+
}
|
|
84
|
+
}, [initialValue, key, deserializer]);
|
|
85
|
+
|
|
86
|
+
const [storedValue, setStoredValue] = useState(() => {
|
|
87
|
+
if (initializeWithValue) {
|
|
88
|
+
return readValue();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return initialValue instanceof Function ? initialValue() : initialValue;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Return a wrapped version of useState's setter function that ...
|
|
95
|
+
// ... persists the new value to localStorage.
|
|
96
|
+
const setValue: Dispatch<SetStateAction<T>> = useEventCallback((value) => {
|
|
97
|
+
// Prevent build error "window is undefined" but keeps working
|
|
98
|
+
if (IS_SERVER) {
|
|
99
|
+
console.warn(
|
|
100
|
+
`Tried setting localStorage key “${key}” even though environment is not a client`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Allow value to be a function so we have the same API as useState
|
|
106
|
+
const newValue = value instanceof Function ? value(readValue()) : value;
|
|
107
|
+
|
|
108
|
+
// Save to local storage
|
|
109
|
+
window.localStorage.setItem(key, serializer(newValue));
|
|
110
|
+
|
|
111
|
+
// Save state
|
|
112
|
+
setStoredValue(newValue);
|
|
113
|
+
|
|
114
|
+
// We dispatch a custom event so every similar useLocalStorage hook is notified
|
|
115
|
+
window.dispatchEvent(new StorageEvent("local-storage", { key }));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.warn(`Error setting localStorage key “${key}”:`, error);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const removeValue = useEventCallback(() => {
|
|
122
|
+
// Prevent build error "window is undefined" but keeps working
|
|
123
|
+
if (IS_SERVER) {
|
|
124
|
+
console.warn(
|
|
125
|
+
`Tried removing localStorage key “${key}” even though environment is not a client`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const defaultValue =
|
|
130
|
+
initialValue instanceof Function ? initialValue() : initialValue;
|
|
131
|
+
|
|
132
|
+
// Remove the key from local storage
|
|
133
|
+
window.localStorage.removeItem(key);
|
|
134
|
+
|
|
135
|
+
// Save state with default value
|
|
136
|
+
setStoredValue(defaultValue);
|
|
137
|
+
|
|
138
|
+
// We dispatch a custom event so every similar useLocalStorage hook is notified
|
|
139
|
+
window.dispatchEvent(new StorageEvent("local-storage", { key }));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
setStoredValue(readValue());
|
|
144
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
145
|
+
}, [key]);
|
|
146
|
+
|
|
147
|
+
const handleStorageChange = useCallback(
|
|
148
|
+
(event: StorageEvent | CustomEvent) => {
|
|
149
|
+
if ((event as StorageEvent).key && (event as StorageEvent).key !== key) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
setStoredValue(readValue());
|
|
153
|
+
},
|
|
154
|
+
[key, readValue],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// this only works for other documents, not the current one
|
|
158
|
+
useEventListener("storage", handleStorageChange);
|
|
159
|
+
|
|
160
|
+
// this is a custom event, triggered in writeValueToLocalStorage
|
|
161
|
+
// See: useLocalStorage()
|
|
162
|
+
useEventListener("local-storage", handleStorageChange);
|
|
163
|
+
|
|
164
|
+
return [storedValue, setValue, removeValue];
|
|
165
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
interface PositionMatrix {
|
|
5
|
+
x: number | undefined;
|
|
6
|
+
y: number | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type Event = MouseEvent | undefined;
|
|
10
|
+
|
|
11
|
+
export function useMousePosition({ includeTouch }: { includeTouch: Boolean }) {
|
|
12
|
+
const [mousePosition, setMousePosition] = useState<PositionMatrix>({
|
|
13
|
+
x: undefined,
|
|
14
|
+
y: undefined,
|
|
15
|
+
});
|
|
16
|
+
const [touchPosition, setTouchPosition] = useState<PositionMatrix>({
|
|
17
|
+
x: undefined,
|
|
18
|
+
y: undefined,
|
|
19
|
+
});
|
|
20
|
+
const [mouseSpeed, setMouseSpeed] = useState(0);
|
|
21
|
+
const [prevEvent, setPrevEvent] = useState<Event>(undefined);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const updateMousePosition = (currentEvent: MouseEvent) => {
|
|
24
|
+
let x, y;
|
|
25
|
+
[x, y] = [currentEvent.clientX, currentEvent.clientY];
|
|
26
|
+
var movementX = Math.abs(
|
|
27
|
+
currentEvent.clientX - (prevEvent?.clientX ? prevEvent?.clientX : 0)
|
|
28
|
+
);
|
|
29
|
+
var movementY = Math.abs(
|
|
30
|
+
currentEvent.clientY - (prevEvent?.clientY ? prevEvent?.clientY : 0)
|
|
31
|
+
);
|
|
32
|
+
var movement = Math.sqrt(movementX * movementX + movementY * movementY);
|
|
33
|
+
var speed = Math.round(10 * movement);
|
|
34
|
+
setMouseSpeed(speed);
|
|
35
|
+
setMousePosition({ x, y });
|
|
36
|
+
setPrevEvent(currentEvent);
|
|
37
|
+
// console.log("prevEvent", prevEvent?.screenX);
|
|
38
|
+
};
|
|
39
|
+
window.addEventListener("mousemove", updateMousePosition);
|
|
40
|
+
return () => {
|
|
41
|
+
window.removeEventListener("mousemove", updateMousePosition);
|
|
42
|
+
};
|
|
43
|
+
}, [prevEvent]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const updateTouchPosition = (currentEvent: TouchEvent) => {
|
|
46
|
+
let x, y;
|
|
47
|
+
if (currentEvent.touches) {
|
|
48
|
+
const touch = currentEvent.touches[0];
|
|
49
|
+
[x, y] = [touch.clientX, touch.clientY];
|
|
50
|
+
}
|
|
51
|
+
setTouchPosition({ x, y });
|
|
52
|
+
};
|
|
53
|
+
return () => {
|
|
54
|
+
if (includeTouch) {
|
|
55
|
+
window.removeEventListener("touchmove", updateTouchPosition);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}, [includeTouch]);
|
|
59
|
+
return { mousePosition, touchPosition, mouseSpeed };
|
|
60
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { useLocalStorage } from "../useLocalStorage/useLocalStorage";
|
|
5
|
+
|
|
6
|
+
type ScreenSize = {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
isSmall: boolean;
|
|
10
|
+
isMedium: boolean;
|
|
11
|
+
isLarge: boolean;
|
|
12
|
+
isXLarge: boolean;
|
|
13
|
+
ltSmall: boolean;
|
|
14
|
+
ltMedium: boolean;
|
|
15
|
+
ltLarge: boolean;
|
|
16
|
+
ltXLarge: boolean;
|
|
17
|
+
gtSmall: boolean;
|
|
18
|
+
gtMedium: boolean;
|
|
19
|
+
gtLarge: boolean;
|
|
20
|
+
} | null;
|
|
21
|
+
|
|
22
|
+
// Move breakpoints OUTSIDE. Prevents Rollup/minifiers from collapsing them.
|
|
23
|
+
const BREAKPOINTS = Object.freeze({
|
|
24
|
+
sm: 600,
|
|
25
|
+
md: 900,
|
|
26
|
+
lg: 1281,
|
|
27
|
+
xl: 1536,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function useScreenSize(): Partial<ScreenSize> {
|
|
31
|
+
const [screenSize, setScreenSize] = useLocalStorage("screen", {
|
|
32
|
+
width: 0,
|
|
33
|
+
height: 0,
|
|
34
|
+
});
|
|
35
|
+
const [size, setSize] = useState<Partial<ScreenSize>>({ ...screenSize });
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const width = screenSize.width;
|
|
39
|
+
const height = screenSize.height;
|
|
40
|
+
setSize({
|
|
41
|
+
width,
|
|
42
|
+
height,
|
|
43
|
+
isSmall: width < BREAKPOINTS.sm,
|
|
44
|
+
isMedium: width >= BREAKPOINTS.sm && width < BREAKPOINTS.md,
|
|
45
|
+
isLarge: width >= BREAKPOINTS.md && width < BREAKPOINTS.lg,
|
|
46
|
+
isXLarge: width >= BREAKPOINTS.lg,
|
|
47
|
+
|
|
48
|
+
ltSmall: width < BREAKPOINTS.sm,
|
|
49
|
+
ltMedium: width < BREAKPOINTS.md,
|
|
50
|
+
ltLarge: width < BREAKPOINTS.lg,
|
|
51
|
+
ltXLarge: width < BREAKPOINTS.xl,
|
|
52
|
+
|
|
53
|
+
gtSmall: width >= BREAKPOINTS.sm,
|
|
54
|
+
gtMedium: width >= BREAKPOINTS.md,
|
|
55
|
+
gtLarge: width >= BREAKPOINTS.lg,
|
|
56
|
+
});
|
|
57
|
+
}, [screenSize]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (typeof window === "undefined") return;
|
|
61
|
+
|
|
62
|
+
const compute = () => {
|
|
63
|
+
const width = window.innerWidth;
|
|
64
|
+
const height = window.innerHeight;
|
|
65
|
+
setScreenSize({ width, height });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
compute();
|
|
69
|
+
window.addEventListener("resize", compute);
|
|
70
|
+
return () => window.removeEventListener("resize", compute);
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
return size;
|
|
74
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
|
|
12
|
+
/* Bundler mode */
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
// Output
|
|
19
|
+
"declaration": true,
|
|
20
|
+
"outDir": "./dist",
|
|
21
|
+
|
|
22
|
+
/* Linting */
|
|
23
|
+
"strict": true,
|
|
24
|
+
"noUnusedLocals": true,
|
|
25
|
+
"noUnusedParameters": true,
|
|
26
|
+
"erasableSyntaxOnly": true,
|
|
27
|
+
"noFallthroughCasesInSwitch": true,
|
|
28
|
+
"noUncheckedSideEffectImports": true
|
|
29
|
+
},
|
|
30
|
+
"include": ["src"]
|
|
31
|
+
}
|