feature: add subscription manage
This commit is contained in:
40
apps/webui/src/presentation/hooks/use-debounded-skeleton.ts
Normal file
40
apps/webui/src/presentation/hooks/use-debounded-skeleton.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useStateRef } from './use-state-ref.ts';
|
||||
export interface UseDebouncedSkeletonProps {
|
||||
minSkeletonDuration?: number;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function useDebouncedSkeleton({
|
||||
minSkeletonDuration = 100,
|
||||
loading,
|
||||
}: UseDebouncedSkeletonProps) {
|
||||
const [showSkeleton, setShowSkeleton, showSkeletonRef] = useStateRef(loading);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading && !showSkeleton) {
|
||||
setShowSkeleton(true);
|
||||
}
|
||||
if (!loading && showSkeleton) {
|
||||
const timeout = setTimeout(() => {
|
||||
if (showSkeletonRef.current) {
|
||||
setShowSkeleton(false);
|
||||
}
|
||||
}, minSkeletonDuration);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
}, [
|
||||
loading,
|
||||
showSkeleton,
|
||||
setShowSkeleton,
|
||||
minSkeletonDuration,
|
||||
showSkeletonRef,
|
||||
]);
|
||||
|
||||
return {
|
||||
showSkeleton,
|
||||
};
|
||||
}
|
||||
18
apps/webui/src/presentation/hooks/use-event.ts
Normal file
18
apps/webui/src/presentation/hooks/use-event.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useCallback, useInsertionEffect, useRef } from 'react';
|
||||
|
||||
export function useEvent<
|
||||
const T extends (
|
||||
...args: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
any[]
|
||||
) => void,
|
||||
>(fn: T): T {
|
||||
const ref = useRef<T | null>(fn);
|
||||
useInsertionEffect(() => {
|
||||
ref.current = fn;
|
||||
}, [fn]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return useCallback((...args: any) => {
|
||||
const latestFn = ref.current!;
|
||||
return latestFn(...args);
|
||||
}, []) as unknown as T;
|
||||
}
|
||||
19
apps/webui/src/presentation/hooks/use-mobile.ts
Normal file
19
apps/webui/src/presentation/hooks/use-mobile.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
28
apps/webui/src/presentation/hooks/use-state-ref.ts
Normal file
28
apps/webui/src/presentation/hooks/use-state-ref.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
type Dispatch,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
export function useStateRef<T>(
|
||||
initialValue: T
|
||||
): [T, Dispatch<SetStateAction<T>>, RefObject<T>] {
|
||||
const [state, _setState] = useState(initialValue);
|
||||
const ref = useRef(initialValue);
|
||||
|
||||
const setState = useCallback((value: T | ((prev: T) => T)) => {
|
||||
let nextValue: T;
|
||||
if (typeof value === 'function') {
|
||||
nextValue = (value as (prev: T) => T)(ref.current);
|
||||
} else {
|
||||
nextValue = value;
|
||||
}
|
||||
ref.current = nextValue;
|
||||
_setState(nextValue);
|
||||
}, []);
|
||||
|
||||
return [state, setState, ref] as const;
|
||||
}
|
||||
Reference in New Issue
Block a user