Vollständiges Redesign mit Gold-Formularen, BMP-Logo, multi-step Forms (Bewerbung, Kontakt, Sponsoring), Netlify-Deployment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1368 lines
32 KiB
JavaScript
1368 lines
32 KiB
JavaScript
import * as React from "react";
|
|
|
|
function isShallowEqual(object1, object2) {
|
|
const keys1 = Object.keys(object1);
|
|
const keys2 = Object.keys(object2);
|
|
|
|
if (keys1.length !== keys2.length) {
|
|
return false;
|
|
}
|
|
|
|
for (let key of keys1) {
|
|
if (object1[key] !== object2[key]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function isTouchEvent({ nativeEvent }) {
|
|
return window.TouchEvent
|
|
? nativeEvent instanceof TouchEvent
|
|
: "touches" in nativeEvent;
|
|
}
|
|
|
|
function isMouseEvent(event) {
|
|
return event.nativeEvent instanceof MouseEvent;
|
|
}
|
|
|
|
function throttle(cb, ms) {
|
|
let lastTime = 0;
|
|
return () => {
|
|
const now = Date.now();
|
|
if (now - lastTime >= ms) {
|
|
cb();
|
|
lastTime = now;
|
|
}
|
|
};
|
|
}
|
|
|
|
function isPlainObject(value) {
|
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
}
|
|
|
|
function dispatchStorageEvent(key, newValue) {
|
|
window.dispatchEvent(new StorageEvent("storage", { key, newValue }));
|
|
}
|
|
|
|
export function useBattery() {
|
|
const [state, setState] = React.useState({
|
|
supported: true,
|
|
loading: true,
|
|
level: null,
|
|
charging: null,
|
|
chargingTime: null,
|
|
dischargingTime: null,
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
if (!navigator.getBattery) {
|
|
setState((s) => ({
|
|
...s,
|
|
supported: false,
|
|
loading: false,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
let battery = null;
|
|
|
|
const handleChange = () => {
|
|
setState({
|
|
supported: true,
|
|
loading: false,
|
|
level: battery.level,
|
|
charging: battery.charging,
|
|
chargingTime: battery.chargingTime,
|
|
dischargingTime: battery.dischargingTime,
|
|
});
|
|
};
|
|
|
|
navigator.getBattery().then((b) => {
|
|
battery = b;
|
|
handleChange();
|
|
|
|
b.addEventListener("levelchange", handleChange);
|
|
b.addEventListener("chargingchange", handleChange);
|
|
b.addEventListener("chargingtimechange", handleChange);
|
|
b.addEventListener("dischargingtimechange", handleChange);
|
|
});
|
|
|
|
return () => {
|
|
if (battery) {
|
|
battery.removeEventListener("levelchange", handleChange);
|
|
battery.removeEventListener("chargingchange", handleChange);
|
|
battery.removeEventListener("chargingtimechange", handleChange);
|
|
battery.removeEventListener("dischargingtimechange", handleChange);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return state;
|
|
}
|
|
|
|
export function useClickAway(cb) {
|
|
const ref = React.useRef(null);
|
|
const refCb = React.useRef(cb);
|
|
|
|
React.useLayoutEffect(() => {
|
|
refCb.current = cb;
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
const handler = (e) => {
|
|
const element = ref.current;
|
|
if (element && !element.contains(e.target)) {
|
|
refCb.current(e);
|
|
}
|
|
};
|
|
|
|
document.addEventListener("mousedown", handler);
|
|
document.addEventListener("touchstart", handler);
|
|
|
|
return () => {
|
|
document.removeEventListener("mousedown", handler);
|
|
document.removeEventListener("touchstart", handler);
|
|
};
|
|
}, []);
|
|
|
|
return ref;
|
|
}
|
|
|
|
function oldSchoolCopy(text) {
|
|
const tempTextArea = document.createElement("textarea");
|
|
tempTextArea.value = text;
|
|
document.body.appendChild(tempTextArea);
|
|
tempTextArea.select();
|
|
document.execCommand("copy");
|
|
document.body.removeChild(tempTextArea);
|
|
}
|
|
|
|
export function useCopyToClipboard() {
|
|
const [state, setState] = React.useState(null);
|
|
|
|
const copyToClipboard = React.useCallback((value) => {
|
|
const handleCopy = async () => {
|
|
try {
|
|
if (navigator?.clipboard?.writeText) {
|
|
await navigator.clipboard.writeText(value);
|
|
setState(value);
|
|
} else {
|
|
throw new Error("writeText not supported");
|
|
}
|
|
} catch (e) {
|
|
oldSchoolCopy(value);
|
|
setState(value);
|
|
}
|
|
};
|
|
|
|
handleCopy();
|
|
}, []);
|
|
|
|
return [state, copyToClipboard];
|
|
}
|
|
|
|
export function useCounter(startingValue = 0, options = {}) {
|
|
const { min, max } = options;
|
|
|
|
if (typeof min === "number" && startingValue < min) {
|
|
throw new Error(
|
|
`Your starting value of ${startingValue} is less than your min of ${min}.`
|
|
);
|
|
}
|
|
|
|
if (typeof max === "number" && startingValue > max) {
|
|
throw new Error(
|
|
`Your starting value of ${startingValue} is greater than your max of ${max}.`
|
|
);
|
|
}
|
|
|
|
const [count, setCount] = React.useState(startingValue);
|
|
|
|
const increment = React.useCallback(() => {
|
|
setCount((c) => {
|
|
const nextCount = c + 1;
|
|
|
|
if (typeof max === "number" && nextCount > max) {
|
|
return c;
|
|
}
|
|
|
|
return nextCount;
|
|
});
|
|
}, [max]);
|
|
|
|
const decrement = React.useCallback(() => {
|
|
setCount((c) => {
|
|
const nextCount = c - 1;
|
|
|
|
if (typeof min === "number" && nextCount < min) {
|
|
return c;
|
|
}
|
|
|
|
return nextCount;
|
|
});
|
|
}, [min]);
|
|
|
|
const set = React.useCallback(
|
|
(nextCount) => {
|
|
setCount((c) => {
|
|
if (typeof max === "number" && nextCount > max) {
|
|
return c;
|
|
}
|
|
|
|
if (typeof min === "number" && nextCount < min) {
|
|
return c;
|
|
}
|
|
|
|
return nextCount;
|
|
});
|
|
},
|
|
[max, min]
|
|
);
|
|
|
|
const reset = React.useCallback(() => {
|
|
setCount(startingValue);
|
|
}, [startingValue]);
|
|
|
|
return [
|
|
count,
|
|
{
|
|
increment,
|
|
decrement,
|
|
set,
|
|
reset,
|
|
},
|
|
];
|
|
}
|
|
|
|
export function useDebounce(value, delay) {
|
|
const [debouncedValue, setDebouncedValue] = React.useState(value);
|
|
|
|
React.useEffect(() => {
|
|
const handler = setTimeout(() => {
|
|
setDebouncedValue(value);
|
|
}, delay);
|
|
|
|
return () => {
|
|
clearTimeout(handler);
|
|
};
|
|
}, [value, delay]);
|
|
|
|
return debouncedValue;
|
|
}
|
|
|
|
export function useDefault(initialValue, defaultValue) {
|
|
const [state, setState] = React.useState(initialValue);
|
|
|
|
if (typeof state === "undefined" || state === null) {
|
|
return [defaultValue, setState];
|
|
}
|
|
|
|
return [state, setState];
|
|
}
|
|
|
|
export function useDocumentTitle(title) {
|
|
React.useEffect(() => {
|
|
document.title = title;
|
|
}, [title]);
|
|
}
|
|
|
|
export function useFavicon(url) {
|
|
React.useEffect(() => {
|
|
let link = document.querySelector(`link[rel~="icon"]`);
|
|
|
|
if (!link) {
|
|
link = document.createElement("link");
|
|
link.type = "image/x-icon";
|
|
link.rel = "icon";
|
|
link.href = url;
|
|
document.head.appendChild(link);
|
|
} else {
|
|
link.href = url;
|
|
}
|
|
}, [url]);
|
|
}
|
|
|
|
export function useGeolocation(options = {}) {
|
|
const [state, setState] = React.useState({
|
|
loading: true,
|
|
accuracy: null,
|
|
altitude: null,
|
|
altitudeAccuracy: null,
|
|
heading: null,
|
|
latitude: null,
|
|
longitude: null,
|
|
speed: null,
|
|
timestamp: null,
|
|
error: null,
|
|
});
|
|
|
|
const optionsRef = React.useRef(options);
|
|
|
|
React.useEffect(() => {
|
|
const onEvent = ({ coords, timestamp }) => {
|
|
setState({
|
|
loading: false,
|
|
timestamp,
|
|
latitude: coords.latitude,
|
|
longitude: coords.longitude,
|
|
altitude: coords.altitude,
|
|
accuracy: coords.accuracy,
|
|
altitudeAccuracy: coords.altitudeAccuracy,
|
|
heading: coords.heading,
|
|
speed: coords.speed,
|
|
});
|
|
};
|
|
|
|
const onEventError = (error) => {
|
|
setState((s) => ({
|
|
...s,
|
|
loading: false,
|
|
error,
|
|
}));
|
|
};
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
onEvent,
|
|
onEventError,
|
|
optionsRef.current
|
|
);
|
|
|
|
const watchId = navigator.geolocation.watchPosition(
|
|
onEvent,
|
|
onEventError,
|
|
optionsRef.current
|
|
);
|
|
|
|
return () => {
|
|
navigator.geolocation.clearWatch(watchId);
|
|
};
|
|
}, []);
|
|
|
|
return state;
|
|
}
|
|
|
|
const initialUseHistoryStateState = {
|
|
past: [],
|
|
present: null,
|
|
future: [],
|
|
};
|
|
|
|
const useHistoryStateReducer = (state, action) => {
|
|
const { past, present, future } = state;
|
|
|
|
if (action.type === "UNDO") {
|
|
return {
|
|
past: past.slice(0, past.length - 1),
|
|
present: past[past.length - 1],
|
|
future: [present, ...future],
|
|
};
|
|
} else if (action.type === "REDO") {
|
|
return {
|
|
past: [...past, present],
|
|
present: future[0],
|
|
future: future.slice(1),
|
|
};
|
|
} else if (action.type === "SET") {
|
|
const { newPresent } = action;
|
|
|
|
if (action.newPresent === present) {
|
|
return state;
|
|
}
|
|
|
|
return {
|
|
past: [...past, present],
|
|
present: newPresent,
|
|
future: [],
|
|
};
|
|
} else if (action.type === "CLEAR") {
|
|
return {
|
|
...initialUseHistoryStateState,
|
|
present: action.initialPresent,
|
|
};
|
|
} else {
|
|
throw new Error("Unsupported action type");
|
|
}
|
|
};
|
|
|
|
export function useHistoryState(initialPresent = {}) {
|
|
const initialPresentRef = React.useRef(initialPresent);
|
|
|
|
const [state, dispatch] = React.useReducer(useHistoryStateReducer, {
|
|
...initialUseHistoryStateState,
|
|
present: initialPresentRef.current,
|
|
});
|
|
|
|
const canUndo = state.past.length !== 0;
|
|
const canRedo = state.future.length !== 0;
|
|
|
|
const undo = React.useCallback(() => {
|
|
if (canUndo) {
|
|
dispatch({ type: "UNDO" });
|
|
}
|
|
}, [canUndo]);
|
|
|
|
const redo = React.useCallback(() => {
|
|
if (canRedo) {
|
|
dispatch({ type: "REDO" });
|
|
}
|
|
}, [canRedo]);
|
|
|
|
const set = React.useCallback(
|
|
(newPresent) => dispatch({ type: "SET", newPresent }),
|
|
[]
|
|
);
|
|
|
|
const clear = React.useCallback(
|
|
() =>
|
|
dispatch({ type: "CLEAR", initialPresent: initialPresentRef.current }),
|
|
[]
|
|
);
|
|
|
|
return { state: state.present, set, undo, redo, clear, canUndo, canRedo };
|
|
}
|
|
|
|
export function useHover() {
|
|
const [hovering, setHovering] = React.useState(false);
|
|
const previousNode = React.useRef(null);
|
|
|
|
const handleMouseEnter = React.useCallback(() => {
|
|
setHovering(true);
|
|
}, []);
|
|
|
|
const handleMouseLeave = React.useCallback(() => {
|
|
setHovering(false);
|
|
}, []);
|
|
|
|
const customRef = React.useCallback(
|
|
(node) => {
|
|
if (previousNode.current?.nodeType === Node.ELEMENT_NODE) {
|
|
previousNode.current.removeEventListener(
|
|
"mouseenter",
|
|
handleMouseEnter
|
|
);
|
|
previousNode.current.removeEventListener(
|
|
"mouseleave",
|
|
handleMouseLeave
|
|
);
|
|
}
|
|
|
|
if (node?.nodeType === Node.ELEMENT_NODE) {
|
|
node.addEventListener("mouseenter", handleMouseEnter);
|
|
node.addEventListener("mouseleave", handleMouseLeave);
|
|
}
|
|
|
|
previousNode.current = node;
|
|
},
|
|
[handleMouseEnter, handleMouseLeave]
|
|
);
|
|
|
|
return [customRef, hovering];
|
|
}
|
|
|
|
export function useIdle(ms = 1000 * 60) {
|
|
const [idle, setIdle] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
let timeoutId;
|
|
|
|
const handleTimeout = () => {
|
|
setIdle(true);
|
|
};
|
|
|
|
const handleEvent = throttle((e) => {
|
|
setIdle(false);
|
|
|
|
window.clearTimeout(timeoutId);
|
|
timeoutId = window.setTimeout(handleTimeout, ms);
|
|
}, 500);
|
|
|
|
const handleVisibilityChange = () => {
|
|
if (!document.hidden) {
|
|
handleEvent();
|
|
}
|
|
};
|
|
|
|
timeoutId = window.setTimeout(handleTimeout, ms);
|
|
|
|
window.addEventListener("mousemove", handleEvent);
|
|
window.addEventListener("mousedown", handleEvent);
|
|
window.addEventListener("resize", handleEvent);
|
|
window.addEventListener("keydown", handleEvent);
|
|
window.addEventListener("touchstart", handleEvent);
|
|
window.addEventListener("wheel", handleEvent);
|
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
|
|
return () => {
|
|
window.removeEventListener("mousemove", handleEvent);
|
|
window.removeEventListener("mousedown", handleEvent);
|
|
window.removeEventListener("resize", handleEvent);
|
|
window.removeEventListener("keydown", handleEvent);
|
|
window.removeEventListener("touchstart", handleEvent);
|
|
window.removeEventListener("wheel", handleEvent);
|
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
window.clearTimeout(timeoutId);
|
|
};
|
|
}, [ms]);
|
|
|
|
return idle;
|
|
}
|
|
|
|
export function useIntersectionObserver(options = {}) {
|
|
const { threshold = 1, root = null, rootMargin = "0px" } = options;
|
|
const [entry, setEntry] = React.useState(null);
|
|
|
|
const previousObserver = React.useRef(null);
|
|
|
|
const customRef = React.useCallback(
|
|
(node) => {
|
|
if (previousObserver.current) {
|
|
previousObserver.current.disconnect();
|
|
previousObserver.current = null;
|
|
}
|
|
|
|
if (node?.nodeType === Node.ELEMENT_NODE) {
|
|
const observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
setEntry(entry);
|
|
},
|
|
{ threshold, root, rootMargin }
|
|
);
|
|
|
|
observer.observe(node);
|
|
previousObserver.current = observer;
|
|
}
|
|
},
|
|
[threshold, root, rootMargin]
|
|
);
|
|
|
|
return [customRef, entry];
|
|
}
|
|
|
|
export function useIsClient() {
|
|
const [isClient, setIsClient] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
setIsClient(true);
|
|
}, []);
|
|
|
|
return isClient;
|
|
}
|
|
|
|
export function useIsFirstRender() {
|
|
const renderRef = React.useRef(true);
|
|
|
|
if (renderRef.current === true) {
|
|
renderRef.current = false;
|
|
return true;
|
|
}
|
|
|
|
return renderRef.current;
|
|
}
|
|
|
|
export function useList(defaultList = []) {
|
|
const [list, setList] = React.useState(defaultList);
|
|
|
|
const set = React.useCallback((l) => {
|
|
setList(l);
|
|
}, []);
|
|
|
|
const push = React.useCallback((element) => {
|
|
setList((l) => [...l, element]);
|
|
}, []);
|
|
|
|
const removeAt = React.useCallback((index) => {
|
|
setList((l) => [...l.slice(0, index), ...l.slice(index + 1)]);
|
|
}, []);
|
|
|
|
const insertAt = React.useCallback((index, element) => {
|
|
setList((l) => [...l.slice(0, index), element, ...l.slice(index)]);
|
|
}, []);
|
|
|
|
const updateAt = React.useCallback((index, element) => {
|
|
setList((l) => l.map((e, i) => (i === index ? element : e)));
|
|
}, []);
|
|
|
|
const clear = React.useCallback(() => setList([]), []);
|
|
|
|
return [list, { set, push, removeAt, insertAt, updateAt, clear }];
|
|
}
|
|
|
|
const setLocalStorageItem = (key, value) => {
|
|
const stringifiedValue = JSON.stringify(value);
|
|
window.localStorage.setItem(key, stringifiedValue);
|
|
dispatchStorageEvent(key, stringifiedValue);
|
|
};
|
|
|
|
const removeLocalStorageItem = (key) => {
|
|
window.localStorage.removeItem(key);
|
|
dispatchStorageEvent(key, null);
|
|
};
|
|
|
|
const getLocalStorageItem = (key) => {
|
|
return window.localStorage.getItem(key);
|
|
};
|
|
|
|
const useLocalStorageSubscribe = (callback) => {
|
|
window.addEventListener("storage", callback);
|
|
return () => window.removeEventListener("storage", callback);
|
|
};
|
|
|
|
const getLocalStorageServerSnapshot = () => {
|
|
throw Error("useLocalStorage is a client-only hook");
|
|
};
|
|
|
|
export function useLocalStorage(key, initialValue) {
|
|
const getSnapshot = () => getLocalStorageItem(key);
|
|
|
|
const store = React.useSyncExternalStore(
|
|
useLocalStorageSubscribe,
|
|
getSnapshot,
|
|
getLocalStorageServerSnapshot
|
|
);
|
|
|
|
const setState = React.useCallback(
|
|
(v) => {
|
|
try {
|
|
const nextState = typeof v === "function" ? v(JSON.parse(store)) : v;
|
|
|
|
if (nextState === undefined || nextState === null) {
|
|
removeLocalStorageItem(key);
|
|
} else {
|
|
setLocalStorageItem(key, nextState);
|
|
}
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
},
|
|
[key, store]
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
if (
|
|
getLocalStorageItem(key) === null &&
|
|
typeof initialValue !== "undefined"
|
|
) {
|
|
setLocalStorageItem(key, initialValue);
|
|
}
|
|
}, [key, initialValue]);
|
|
|
|
return [store ? JSON.parse(store) : initialValue, setState];
|
|
}
|
|
|
|
export function useLockBodyScroll() {
|
|
React.useLayoutEffect(() => {
|
|
const originalStyle = window.getComputedStyle(document.body).overflow;
|
|
document.body.style.overflow = "hidden";
|
|
return () => {
|
|
document.body.style.overflow = originalStyle;
|
|
};
|
|
}, []);
|
|
}
|
|
|
|
export function useLongPress(callback, options = {}) {
|
|
const { threshold = 400, onStart, onFinish, onCancel } = options;
|
|
const isLongPressActive = React.useRef(false);
|
|
const isPressed = React.useRef(false);
|
|
const timerId = React.useRef();
|
|
|
|
return React.useMemo(() => {
|
|
if (typeof callback !== "function") {
|
|
return {};
|
|
}
|
|
|
|
const start = (event) => {
|
|
if (!isMouseEvent(event) && !isTouchEvent(event)) return;
|
|
|
|
if (onStart) {
|
|
onStart(event);
|
|
}
|
|
|
|
isPressed.current = true;
|
|
timerId.current = setTimeout(() => {
|
|
callback(event);
|
|
isLongPressActive.current = true;
|
|
}, threshold);
|
|
};
|
|
|
|
const cancel = (event) => {
|
|
if (!isMouseEvent(event) && !isTouchEvent(event)) return;
|
|
|
|
if (isLongPressActive.current) {
|
|
if (onFinish) {
|
|
onFinish(event);
|
|
}
|
|
} else if (isPressed.current) {
|
|
if (onCancel) {
|
|
onCancel(event);
|
|
}
|
|
}
|
|
|
|
isLongPressActive.current = false;
|
|
isPressed.current = false;
|
|
|
|
if (timerId.current) {
|
|
window.clearTimeout(timerId.current);
|
|
}
|
|
};
|
|
|
|
const mouseHandlers = {
|
|
onMouseDown: start,
|
|
onMouseUp: cancel,
|
|
onMouseLeave: cancel,
|
|
};
|
|
|
|
const touchHandlers = {
|
|
onTouchStart: start,
|
|
onTouchEnd: cancel,
|
|
};
|
|
|
|
return {
|
|
...mouseHandlers,
|
|
...touchHandlers,
|
|
};
|
|
}, [callback, threshold, onCancel, onFinish, onStart]);
|
|
}
|
|
|
|
export function useMap(initialState) {
|
|
const mapRef = React.useRef(new Map(initialState));
|
|
const [, reRender] = React.useReducer((x) => x + 1, 0);
|
|
|
|
mapRef.current.set = (...args) => {
|
|
Map.prototype.set.apply(mapRef.current, args);
|
|
reRender();
|
|
return mapRef.current;
|
|
};
|
|
|
|
mapRef.current.clear = (...args) => {
|
|
Map.prototype.clear.apply(mapRef.current, args);
|
|
reRender();
|
|
};
|
|
|
|
mapRef.current.delete = (...args) => {
|
|
const res = Map.prototype.delete.apply(mapRef.current, args);
|
|
reRender();
|
|
|
|
return res;
|
|
};
|
|
|
|
return mapRef.current;
|
|
}
|
|
|
|
export function useMeasure() {
|
|
const [dimensions, setDimensions] = React.useState({
|
|
width: null,
|
|
height: null,
|
|
});
|
|
|
|
const previousObserver = React.useRef(null);
|
|
|
|
const customRef = React.useCallback((node) => {
|
|
if (previousObserver.current) {
|
|
previousObserver.current.disconnect();
|
|
previousObserver.current = null;
|
|
}
|
|
|
|
if (node?.nodeType === Node.ELEMENT_NODE) {
|
|
const observer = new ResizeObserver(([entry]) => {
|
|
if (entry && entry.borderBoxSize) {
|
|
const { inlineSize: width, blockSize: height } =
|
|
entry.borderBoxSize[0];
|
|
|
|
setDimensions({ width, height });
|
|
}
|
|
});
|
|
|
|
observer.observe(node);
|
|
previousObserver.current = observer;
|
|
}
|
|
}, []);
|
|
|
|
return [customRef, dimensions];
|
|
}
|
|
|
|
export function useMediaQuery(query) {
|
|
const subscribe = React.useCallback(
|
|
(callback) => {
|
|
const matchMedia = window.matchMedia(query);
|
|
|
|
matchMedia.addEventListener("change", callback);
|
|
return () => {
|
|
matchMedia.removeEventListener("change", callback);
|
|
};
|
|
},
|
|
[query]
|
|
);
|
|
|
|
const getSnapshot = () => {
|
|
return window.matchMedia(query).matches;
|
|
};
|
|
|
|
const getServerSnapshot = () => {
|
|
throw Error("useMediaQuery is a client-only hook");
|
|
};
|
|
|
|
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
}
|
|
|
|
export function useMouse() {
|
|
const [state, setState] = React.useState({
|
|
x: 0,
|
|
y: 0,
|
|
elementX: 0,
|
|
elementY: 0,
|
|
elementPositionX: 0,
|
|
elementPositionY: 0,
|
|
});
|
|
|
|
const ref = React.useRef(null);
|
|
|
|
React.useLayoutEffect(() => {
|
|
const handleMouseMove = (event) => {
|
|
let newState = {
|
|
x: event.pageX,
|
|
y: event.pageY,
|
|
};
|
|
|
|
if (ref.current?.nodeType === Node.ELEMENT_NODE) {
|
|
const { left, top } = ref.current.getBoundingClientRect();
|
|
const elementPositionX = left + window.scrollX;
|
|
const elementPositionY = top + window.scrollY;
|
|
const elementX = event.pageX - elementPositionX;
|
|
const elementY = event.pageY - elementPositionY;
|
|
|
|
newState.elementX = elementX;
|
|
newState.elementY = elementY;
|
|
newState.elementPositionX = elementPositionX;
|
|
newState.elementPositionY = elementPositionY;
|
|
}
|
|
|
|
setState((s) => {
|
|
return {
|
|
...s,
|
|
...newState,
|
|
};
|
|
});
|
|
};
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
|
|
return () => {
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
};
|
|
}, []);
|
|
|
|
return [state, ref];
|
|
}
|
|
|
|
const getConnection = () => {
|
|
return (
|
|
navigator?.connection ||
|
|
navigator?.mozConnection ||
|
|
navigator?.webkitConnection
|
|
);
|
|
};
|
|
|
|
const useNetworkStateSubscribe = (callback) => {
|
|
window.addEventListener("online", callback, { passive: true });
|
|
window.addEventListener("offline", callback, { passive: true });
|
|
|
|
const connection = getConnection();
|
|
|
|
if (connection) {
|
|
connection.addEventListener("change", callback, { passive: true });
|
|
}
|
|
|
|
return () => {
|
|
window.removeEventListener("online", callback);
|
|
window.removeEventListener("offline", callback);
|
|
|
|
if (connection) {
|
|
connection.removeEventListener("change", callback);
|
|
}
|
|
};
|
|
};
|
|
|
|
const getNetworkStateServerSnapshot = () => {
|
|
throw Error("useNetworkState is a client-only hook");
|
|
};
|
|
|
|
export function useNetworkState() {
|
|
const cache = React.useRef({});
|
|
|
|
const getSnapshot = () => {
|
|
const online = navigator.onLine;
|
|
const connection = getConnection();
|
|
|
|
const nextState = {
|
|
online,
|
|
downlink: connection?.downlink,
|
|
downlinkMax: connection?.downlinkMax,
|
|
effectiveType: connection?.effectiveType,
|
|
rtt: connection?.rtt,
|
|
saveData: connection?.saveData,
|
|
type: connection?.type,
|
|
};
|
|
|
|
if (isShallowEqual(cache.current, nextState)) {
|
|
return cache.current;
|
|
} else {
|
|
cache.current = nextState;
|
|
return nextState;
|
|
}
|
|
};
|
|
|
|
return React.useSyncExternalStore(
|
|
useNetworkStateSubscribe,
|
|
getSnapshot,
|
|
getNetworkStateServerSnapshot
|
|
);
|
|
}
|
|
|
|
export function useObjectState(initialValue) {
|
|
const [state, setState] = React.useState(initialValue);
|
|
|
|
const handleUpdate = React.useCallback((arg) => {
|
|
if (typeof arg === "function") {
|
|
setState((s) => {
|
|
const newState = arg(s);
|
|
|
|
if (isPlainObject(newState)) {
|
|
return {
|
|
...s,
|
|
...newState,
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
if (isPlainObject(arg)) {
|
|
setState((s) => ({
|
|
...s,
|
|
...arg,
|
|
}));
|
|
}
|
|
}, []);
|
|
|
|
return [state, handleUpdate];
|
|
}
|
|
|
|
export function useOrientation() {
|
|
const [orientation, setOrientation] = React.useState({
|
|
angle: 0,
|
|
type: "landscape-primary",
|
|
});
|
|
|
|
React.useLayoutEffect(() => {
|
|
const handleChange = () => {
|
|
const { angle, type } = window.screen.orientation;
|
|
setOrientation({
|
|
angle,
|
|
type,
|
|
});
|
|
};
|
|
|
|
const handle_orientationchange = () => {
|
|
setOrientation({
|
|
type: "UNKNOWN",
|
|
angle: window.orientation,
|
|
});
|
|
};
|
|
|
|
if (window.screen?.orientation) {
|
|
handleChange();
|
|
window.screen.orientation.addEventListener("change", handleChange);
|
|
} else {
|
|
handle_orientationchange();
|
|
window.addEventListener("orientationchange", handle_orientationchange);
|
|
}
|
|
|
|
return () => {
|
|
if (window.screen?.orientation) {
|
|
window.screen.orientation.removeEventListener("change", handleChange);
|
|
} else {
|
|
window.removeEventListener(
|
|
"orientationchange",
|
|
handle_orientationchange
|
|
);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return orientation;
|
|
}
|
|
|
|
const usePreferredLanguageSubscribe = (cb) => {
|
|
window.addEventListener("languagechange", cb);
|
|
return () => window.removeEventListener("languagechange", cb);
|
|
};
|
|
|
|
const getPreferredLanguageSnapshot = () => {
|
|
return navigator.language;
|
|
};
|
|
|
|
const getPreferredLanguageServerSnapshot = () => {
|
|
throw Error("usePreferredLanguage is a client-only hook");
|
|
};
|
|
|
|
export function usePreferredLanguage() {
|
|
return React.useSyncExternalStore(
|
|
usePreferredLanguageSubscribe,
|
|
getPreferredLanguageSnapshot,
|
|
getPreferredLanguageServerSnapshot
|
|
);
|
|
}
|
|
|
|
export function usePrevious(value) {
|
|
const [current, setCurrent] = React.useState(value);
|
|
const [previous, setPrevious] = React.useState(null);
|
|
|
|
if (value !== current) {
|
|
setPrevious(current);
|
|
setCurrent(value);
|
|
}
|
|
|
|
return previous;
|
|
}
|
|
|
|
export function useQueue(initialValue = []) {
|
|
const [queue, setQueue] = React.useState(initialValue);
|
|
|
|
const add = React.useCallback((element) => {
|
|
setQueue((q) => [...q, element]);
|
|
}, []);
|
|
|
|
const remove = React.useCallback(() => {
|
|
let removedElement;
|
|
|
|
setQueue(([first, ...q]) => {
|
|
removedElement = first;
|
|
return q;
|
|
});
|
|
|
|
return removedElement;
|
|
}, []);
|
|
|
|
const clear = React.useCallback(() => {
|
|
setQueue([]);
|
|
}, []);
|
|
|
|
return {
|
|
add,
|
|
remove,
|
|
clear,
|
|
first: queue[0],
|
|
last: queue[queue.length - 1],
|
|
size: queue.length,
|
|
queue,
|
|
};
|
|
}
|
|
|
|
export function useRenderCount() {
|
|
const count = React.useRef(0);
|
|
|
|
count.current++;
|
|
|
|
return count.current;
|
|
}
|
|
|
|
export function useRenderInfo(name = "Unknown") {
|
|
const count = React.useRef(0);
|
|
const lastRender = React.useRef();
|
|
const now = Date.now();
|
|
|
|
count.current++;
|
|
|
|
React.useEffect(() => {
|
|
lastRender.current = Date.now();
|
|
});
|
|
|
|
const sinceLastRender = lastRender.current ? now - lastRender.current : 0;
|
|
|
|
if (process.env.NODE_ENV !== "production") {
|
|
const info = {
|
|
name,
|
|
renders: count.current,
|
|
sinceLastRender,
|
|
timestamp: now,
|
|
};
|
|
|
|
console.log(info);
|
|
|
|
return info;
|
|
}
|
|
}
|
|
|
|
export function useScript(src, options = {}) {
|
|
const [status, setStatus] = React.useState("loading");
|
|
const optionsRef = React.useRef(options);
|
|
|
|
React.useEffect(() => {
|
|
let script = document.querySelector(`script[src="${src}"]`);
|
|
|
|
const domStatus = script?.getAttribute("data-status");
|
|
if (domStatus) {
|
|
setStatus(domStatus);
|
|
return;
|
|
}
|
|
|
|
if (script === null) {
|
|
script = document.createElement("script");
|
|
script.src = src;
|
|
script.async = true;
|
|
script.setAttribute("data-status", "loading");
|
|
document.body.appendChild(script);
|
|
|
|
const handleScriptLoad = () => {
|
|
script.setAttribute("data-status", "ready");
|
|
setStatus("ready");
|
|
removeEventListeners();
|
|
};
|
|
|
|
const handleScriptError = () => {
|
|
script.setAttribute("data-status", "error");
|
|
setStatus("error");
|
|
removeEventListeners();
|
|
};
|
|
|
|
const removeEventListeners = () => {
|
|
script.removeEventListener("load", handleScriptLoad);
|
|
script.removeEventListener("error", handleScriptError);
|
|
};
|
|
|
|
script.addEventListener("load", handleScriptLoad);
|
|
script.addEventListener("error", handleScriptError);
|
|
|
|
const removeOnUnmount = optionsRef.current.removeOnUnmount;
|
|
|
|
return () => {
|
|
if (removeOnUnmount === true) {
|
|
script.remove();
|
|
removeEventListeners();
|
|
}
|
|
};
|
|
} else {
|
|
setStatus("unknown");
|
|
}
|
|
}, [src]);
|
|
|
|
return status;
|
|
}
|
|
|
|
const setSessionStorageItem = (key, value) => {
|
|
const stringifiedValue = JSON.stringify(value);
|
|
window.sessionStorage.setItem(key, stringifiedValue);
|
|
dispatchStorageEvent(key, stringifiedValue);
|
|
};
|
|
|
|
const removeSessionStorageItem = (key) => {
|
|
window.sessionStorage.removeItem(key);
|
|
dispatchStorageEvent(key, null);
|
|
};
|
|
|
|
const getSessionStorageItem = (key) => {
|
|
return window.sessionStorage.getItem(key);
|
|
};
|
|
|
|
const useSessionStorageSubscribe = (callback) => {
|
|
window.addEventListener("storage", callback);
|
|
return () => window.removeEventListener("storage", callback);
|
|
};
|
|
|
|
const getSessionStorageServerSnapshot = () => {
|
|
throw Error("useSessionStorage is a client-only hook");
|
|
};
|
|
|
|
export function useSessionStorage(key, initialValue) {
|
|
const getSnapshot = () => getSessionStorageItem(key);
|
|
|
|
const store = React.useSyncExternalStore(
|
|
useSessionStorageSubscribe,
|
|
getSnapshot,
|
|
getSessionStorageServerSnapshot
|
|
);
|
|
|
|
const setState = React.useCallback(
|
|
(v) => {
|
|
try {
|
|
const nextState = typeof v === "function" ? v(JSON.parse(store)) : v;
|
|
|
|
if (nextState === undefined || nextState === null) {
|
|
removeSessionStorageItem(key);
|
|
} else {
|
|
setSessionStorageItem(key, nextState);
|
|
}
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
},
|
|
[key, store]
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
if (
|
|
getSessionStorageItem(key) === null &&
|
|
typeof initialValue !== "undefined"
|
|
) {
|
|
setSessionStorageItem(key, initialValue);
|
|
}
|
|
}, [key, initialValue]);
|
|
|
|
return [store ? JSON.parse(store) : initialValue, setState];
|
|
}
|
|
|
|
export function useSet(values) {
|
|
const setRef = React.useRef(new Set(values));
|
|
const [, reRender] = React.useReducer((x) => x + 1, 0);
|
|
|
|
setRef.current.add = (...args) => {
|
|
const res = Set.prototype.add.apply(setRef.current, args);
|
|
reRender();
|
|
|
|
return res;
|
|
};
|
|
|
|
setRef.current.clear = (...args) => {
|
|
Set.prototype.clear.apply(setRef.current, args);
|
|
reRender();
|
|
};
|
|
|
|
setRef.current.delete = (...args) => {
|
|
const res = Set.prototype.delete.apply(setRef.current, args);
|
|
reRender();
|
|
|
|
return res;
|
|
};
|
|
|
|
return setRef.current;
|
|
}
|
|
|
|
export function useThrottle(value, interval = 500) {
|
|
const [throttledValue, setThrottledValue] = React.useState(value);
|
|
const lastUpdated = React.useRef(null);
|
|
|
|
React.useEffect(() => {
|
|
const now = Date.now();
|
|
|
|
if (lastUpdated.current && now >= lastUpdated.current + interval) {
|
|
lastUpdated.current = now;
|
|
setThrottledValue(value);
|
|
} else {
|
|
const id = window.setTimeout(() => {
|
|
lastUpdated.current = now;
|
|
setThrottledValue(value);
|
|
}, interval);
|
|
|
|
return () => window.clearTimeout(id);
|
|
}
|
|
}, [value, interval]);
|
|
|
|
return throttledValue;
|
|
}
|
|
|
|
export function useToggle(initialValue) {
|
|
const [on, setOn] = React.useState(() => {
|
|
if (typeof initialValue === "boolean") {
|
|
return initialValue;
|
|
}
|
|
|
|
return Boolean(initialValue);
|
|
});
|
|
|
|
const handleToggle = React.useCallback((value) => {
|
|
if (typeof value === "boolean") {
|
|
return setOn(value);
|
|
}
|
|
|
|
return setOn((v) => !v);
|
|
}, []);
|
|
|
|
return [on, handleToggle];
|
|
}
|
|
|
|
const useVisibilityChangeSubscribe = (callback) => {
|
|
document.addEventListener("visibilitychange", callback);
|
|
|
|
return () => {
|
|
document.removeEventListener("visibilitychange", callback);
|
|
};
|
|
};
|
|
|
|
const getVisibilityChangeSnapshot = () => {
|
|
return document.visibilityState;
|
|
};
|
|
|
|
const getVisibilityChangeServerSnapshot = () => {
|
|
throw Error("useVisibilityChange is a client-only hook");
|
|
};
|
|
|
|
export function useVisibilityChange() {
|
|
const visibilityState = React.useSyncExternalStore(
|
|
useVisibilityChangeSubscribe,
|
|
getVisibilityChangeSnapshot,
|
|
getVisibilityChangeServerSnapshot
|
|
);
|
|
|
|
return visibilityState === "visible";
|
|
}
|
|
|
|
export function useWindowScroll() {
|
|
const [state, setState] = React.useState({
|
|
x: null,
|
|
y: null,
|
|
});
|
|
|
|
const scrollTo = React.useCallback((...args) => {
|
|
if (typeof args[0] === "object") {
|
|
window.scrollTo(args[0]);
|
|
} else if (typeof args[0] === "number" && typeof args[1] === "number") {
|
|
window.scrollTo(args[0], args[1]);
|
|
} else {
|
|
throw new Error(
|
|
`Invalid arguments passed to scrollTo. See here for more info. https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo`
|
|
);
|
|
}
|
|
}, []);
|
|
|
|
React.useLayoutEffect(() => {
|
|
const handleScroll = () => {
|
|
setState({ x: window.scrollX, y: window.scrollY });
|
|
};
|
|
|
|
handleScroll();
|
|
window.addEventListener("scroll", handleScroll);
|
|
|
|
return () => {
|
|
window.removeEventListener("scroll", handleScroll);
|
|
};
|
|
}, []);
|
|
|
|
return [state, scrollTo];
|
|
}
|
|
|
|
export function useWindowSize() {
|
|
const [size, setSize] = React.useState({
|
|
width: null,
|
|
height: null,
|
|
});
|
|
|
|
React.useLayoutEffect(() => {
|
|
const handleResize = () => {
|
|
setSize({
|
|
width: window.innerWidth,
|
|
height: window.innerHeight,
|
|
});
|
|
};
|
|
|
|
handleResize();
|
|
window.addEventListener("resize", handleResize);
|
|
|
|
return () => {
|
|
window.removeEventListener("resize", handleResize);
|
|
};
|
|
}, []);
|
|
|
|
return size;
|
|
}
|