first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
import {useIsSSR} from '@react-aria/ssr';
import {getBootstrapData} from '@common/core/bootstrap-data/use-backend-bootstrap-data';
const MOBILE_SCREEN_WIDTH = 768;
export function useIsMobileDevice(): boolean {
const isSSR = useIsSSR();
if (isSSR || typeof window === 'undefined') {
return getBootstrapData().is_mobile_device;
}
return window.screen.width <= MOBILE_SCREEN_WIDTH;
}

View File

@@ -0,0 +1,5 @@
import { useMediaQuery, UseMediaQueryOptions } from "./use-media-query";
export function useIsMobileMediaQuery(options?: UseMediaQueryOptions) {
return useMediaQuery("(max-width: 768px)", options);
}

View File

@@ -0,0 +1,5 @@
import {useMediaQuery, UseMediaQueryOptions} from './use-media-query';
export function useIsTabletMediaQuery(options?: UseMediaQueryOptions) {
return useMediaQuery('(max-width: 1024px)', options);
}

View File

@@ -0,0 +1,5 @@
import {useMediaQuery} from './use-media-query';
export function useIsTouchDevice() {
return useMediaQuery('((pointer: coarse))');
}

View File

@@ -0,0 +1,75 @@
import {useEffect, useState} from 'react';
interface StoreEvent {
detail: {
key: string;
newValue: any;
};
}
export function useLocalStorage<T>(key: string, initialValue: T | null = null) {
const [storedValue, setStoredValue] = useState<T>(() => {
return getFromLocalStorage<T>(key, initialValue);
});
const setValue = (value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
setInLocalStorage(key, valueToStore);
};
// update state value using custom storage event. This will re-render
// component even if local storage value was set from different hook instance
useEffect(() => {
const handleStorageChange = (event: StoreEvent) => {
if (event.detail?.key === key) {
setStoredValue(event.detail.newValue);
}
};
window.addEventListener('storage', handleStorageChange as any);
return () =>
window.removeEventListener('storage', handleStorageChange as any);
}, [key]);
return [storedValue, setValue] as const;
}
export function getFromLocalStorage<T>(
key: string,
initialValue: T | null = null,
) {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item != null ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
}
export function setInLocalStorage<T>(key: string, value: T) {
try {
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(value));
window.dispatchEvent(
new CustomEvent('storage', {
detail: {key, newValue: value},
}),
);
}
} catch (error) {
//
}
}
export function removeFromLocalStorage(key: string) {
try {
if (typeof window !== 'undefined') {
window.localStorage.removeItem(key);
}
} catch (error) {
//
}
}

View File

@@ -0,0 +1,22 @@
import {useCallback, useRef, useState} from 'react';
export function useStickySentinel() {
const [isSticky, setIsSticky] = useState(false);
const observerRef = useRef<IntersectionObserver>();
const sentinelRef = useCallback((sentinel: HTMLDivElement | null) => {
if (sentinel) {
const observer = new IntersectionObserver(
([e]) => setIsSticky(e.intersectionRatio < 1),
{threshold: [1]}
);
observerRef.current = observer;
observer.observe(sentinel);
} else if (observerRef.current) {
observerRef.current?.disconnect();
}
}, []);
return {isSticky, sentinelRef};
}

View File

@@ -0,0 +1,14 @@
import {useEffect} from 'react';
export function useBlockBodyOverflow(disable: boolean = false) {
useEffect(() => {
if (disable) {
document.documentElement.classList.remove('no-page-overflow');
} else {
document.documentElement.classList.add('no-page-overflow');
}
return () => {
document.documentElement.classList.remove('no-page-overflow');
};
}, [disable]);
}

View File

@@ -0,0 +1,18 @@
import {useEffect, useMemo, useRef} from 'react';
/**
* A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a
* prop or avoid re-executing effects when passed as a dependency
*/
export function useCallbackRef<T extends (...args: any[]) => any>(
callback: T | undefined
): T {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
});
// https://github.com/facebook/react/issues/19240
return useMemo(() => ((...args) => callbackRef.current?.(...args)) as T, []);
}

View File

@@ -0,0 +1,104 @@
import {useCallback, useEffect, useState} from 'react';
import {isSsr} from '@common/utils/dom/is-ssr';
interface Options {
days?: number;
path?: string;
domain?: string;
SameSite?: 'None' | 'Lax' | 'Strict';
Secure?: boolean;
HttpOnly?: boolean;
}
// used to notify different instances of useCookie hook about cookie changes
const listeners = new Set<{name: string; callback: any}>();
const listenForCookieChange = (
name: string,
callback: (value: string) => void,
) => {
if (isSsr()) return () => {};
const listener = {name, callback};
listeners.add(listener);
return () => {
listeners.delete(listener);
};
};
export function stringifyOptions(options: Options) {
return Object.keys(options).reduce((acc, _key) => {
const key = _key as keyof Options;
if (key === 'days') {
return acc;
} else {
if (options[key] === false) {
return acc;
} else if (options[key] === true) {
return `${acc}; ${key}`;
} else {
return `${acc}; ${key}=${options[key]}`;
}
}
}, '');
}
export const setCookie = (name: string, value: string, options?: Options) => {
if (isSsr()) return;
const optionsWithDefaults = {
days: 7,
path: '/',
...options,
};
const expires = new Date(
Date.now() + optionsWithDefaults.days * 864e5,
).toUTCString();
document.cookie =
name +
'=' +
encodeURIComponent(value) +
'; expires=' +
expires +
stringifyOptions(optionsWithDefaults);
listeners.forEach(listener => {
if (listener.name === name) {
listener.callback(value);
}
});
};
export function getCookie(name: string, initialValue = '') {
return (
(!isSsr() &&
document.cookie &&
document.cookie.split('; ').reduce((r, v) => {
const parts = v.split('=');
return parts[0] === name ? decodeURIComponent(parts[1]) : r;
}, '')) ||
initialValue
);
}
export function useCookie(key: string, initialValue?: string) {
const [item, setItem] = useState(() => {
return getCookie(key, initialValue);
});
useEffect(() => {
return listenForCookieChange(key, value => {
setItem(value);
});
}, [key]);
const updateItem = useCallback(
(value: string, options?: Options) => {
setItem(value);
setCookie(key, value, options);
},
[key],
);
return [item, updateItem] as const;
}

View File

@@ -0,0 +1,14 @@
import {useMemo} from 'react';
import linkifyStr from 'linkify-string';
export function useLinkifiedString(text: string | null | undefined) {
return useMemo(() => {
if (!text) {
return text;
}
return linkifyStr(text, {
nl2br: true,
attributes: {rel: 'nofollow'},
});
}, [text]);
}

View File

@@ -0,0 +1,42 @@
import {useEffect, useState} from 'react';
export interface UseMediaQueryOptions {
noSSR?: boolean;
}
export function useMediaQuery(
query: string,
{noSSR}: UseMediaQueryOptions = {noSSR: true}
) {
const supportsMatchMedia =
typeof window !== 'undefined' && typeof window.matchMedia === 'function';
const [matches, setMatches] = useState(
noSSR
? () => (supportsMatchMedia ? window.matchMedia(query).matches : false)
: null
);
useEffect(() => {
if (!supportsMatchMedia) {
return;
}
const mq = window.matchMedia(query);
const onChange = () => {
setMatches(mq.matches);
};
mq.addEventListener('change', onChange);
if (!noSSR) {
onChange();
}
return () => {
mq.removeEventListener('change', onChange);
};
}, [supportsMatchMedia, query, noSSR]);
// If in SSR, the media query should never match. Once the page hydrates,
// this will update and the real value will be returned.
return typeof window === 'undefined' ? null : matches;
}

View File

@@ -0,0 +1,27 @@
import {
createPath,
NavigateFunction,
resolvePath,
useLocation,
useNavigate as useRouterNavigate
} from 'react-router-dom';
import {useCallback} from 'react';
export function useNavigate() {
const routerNavigate = useRouterNavigate();
const location = useLocation();
return useCallback(
(to, options) => {
// prevent duplicates in history when navigating to the same url
const replace =
createPath(location) === createPath(resolvePath(to, location.pathname));
routerNavigate(to, {
...options,
replace: options?.replace !== false && replace,
});
},
[routerNavigate, location]
) as NavigateFunction;
}

View File

@@ -0,0 +1,11 @@
import {useEffect, useRef} from 'react';
export function usePrevious<T>(value: T) {
const ref = useRef<T>();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}

View File

@@ -0,0 +1,62 @@
import {useEffect, useRef, useState} from 'react';
interface SpinDelayOptions {
delay?: number;
minDuration?: number;
}
type State = 'IDLE' | 'DELAY' | 'DISPLAY' | 'EXPIRE';
export const defaultOptions = {
delay: 500,
minDuration: 200,
};
export function useSpinDelay(
loading: boolean,
options?: SpinDelayOptions,
): boolean {
options = Object.assign({}, defaultOptions, options);
const [state, setState] = useState<State>('IDLE');
const timeout = useRef<any>(null);
useEffect(() => {
if (loading && state === 'IDLE') {
clearTimeout(timeout.current);
timeout.current = setTimeout(
() => {
if (!loading) {
return setState('IDLE');
}
timeout.current = setTimeout(
() => {
setState('EXPIRE');
},
options?.minDuration,
);
setState('DISPLAY');
},
options?.delay,
);
setState('DELAY');
}
if (!loading && state !== 'DISPLAY') {
clearTimeout(timeout.current);
setState('IDLE');
}
}, [loading, state, options.delay, options.minDuration]);
useEffect(() => {
return () => clearTimeout(timeout.current);
}, []);
return state === 'DISPLAY' || state === 'EXPIRE';
}
export default useSpinDelay;

View File

@@ -0,0 +1,14 @@
import {useEffect} from 'react';
export function useStableScrollbar(disable: boolean = false) {
useEffect(() => {
if (disable) {
document.documentElement.classList.remove('stable-scrollbar');
} else {
document.documentElement.classList.add('stable-scrollbar');
}
return () => {
document.documentElement.classList.remove('stable-scrollbar');
};
}, [disable]);
}