Clean and re-organize post table refactoring (#1000)

* Clean and re-organize post table refactoring

* Fix tests
This commit is contained in:
Charles Bochet
2023-07-30 18:26:32 -07:00
committed by GitHub
parent 86a2d67efd
commit ade5e52e55
336 changed files with 638 additions and 2757 deletions

View File

@ -0,0 +1,29 @@
import { motion } from 'framer-motion';
type Props = Omit<
React.ComponentProps<typeof motion.div>,
'initial' | 'animated' | 'transition'
> & {
duration?: number;
};
export function AnimatedEaseIn({
children,
duration = 0.3,
...restProps
}: Props) {
const initial = { opacity: 0 };
const animate = { opacity: 1 };
const transition = { ease: 'linear', duration };
return (
<motion.div
initial={initial}
animate={animate}
transition={transition}
{...restProps}
>
{children}
</motion.div>
);
}

View File

@ -0,0 +1,70 @@
import React, { useMemo } from 'react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
const StyledContainer = styled(motion.div)`
display: flex;
overflow: hidden;
`;
const Word = styled(motion.span)`
white-space: pre;
`;
type Props = Omit<React.ComponentProps<typeof motion.div>, 'children'> & {
text: string;
};
const containerAnimation = {
hidden: { opacity: 0 },
visible: (i = 1) => ({
opacity: 1,
transition: { staggerChildren: 0.12, delayChildren: 0.04 * i },
}),
};
const childAnimation = {
visible: {
opacity: 1,
x: 0,
transition: {
type: 'spring',
damping: 12,
stiffness: 100,
},
},
hidden: {
opacity: 0,
x: 20,
transition: {
type: 'spring',
damping: 12,
stiffness: 100,
},
},
};
export function AnimatedTextWord({ text = '', ...restProps }: Props) {
const words = useMemo(() => {
const words = text.split(' ');
return words.map((value, index) =>
index === words.length - 1 ? value : value + ' ',
);
}, [text]);
return (
<StyledContainer
variants={containerAnimation}
initial="hidden"
animate="visible"
{...restProps}
>
{words.map((word, index) => (
<Word variants={childAnimation} key={index}>
{word}
</Word>
))}
</StyledContainer>
);
}

View File

@ -0,0 +1,39 @@
import { useRef } from 'react';
import { fireEvent, render } from '@testing-library/react';
import { useListenClickOutside } from '../useListenClickOutside';
const onOutsideClick = jest.fn();
function TestComponentDomMode() {
const buttonRef = useRef(null);
const buttonRef2 = useRef(null);
useListenClickOutside({
refs: [buttonRef, buttonRef2],
callback: onOutsideClick,
});
return (
<div>
<span>Outside</span>
<button ref={buttonRef}>Inside</button>
<button ref={buttonRef2}>Inside 2</button>
</div>
);
}
test('useListenClickOutside hook works in dom mode', async () => {
const { getByText } = render(<TestComponentDomMode />);
const inside = getByText('Inside');
const inside2 = getByText('Inside 2');
const outside = getByText('Outside');
fireEvent.click(inside);
expect(onOutsideClick).toHaveBeenCalledTimes(0);
fireEvent.click(inside2);
expect(onOutsideClick).toHaveBeenCalledTimes(0);
fireEvent.click(outside);
expect(onOutsideClick).toHaveBeenCalledTimes(1);
});

View File

@ -0,0 +1,78 @@
import React, { useEffect } from 'react';
export enum ClickOutsideMode {
absolute = 'absolute',
dom = 'dom',
}
export function useListenClickOutside<T extends Element>({
refs,
callback,
mode = ClickOutsideMode.dom,
}: {
refs: Array<React.RefObject<T>>;
callback: (event: MouseEvent | TouchEvent) => void;
mode?: ClickOutsideMode;
}) {
useEffect(() => {
function handleClickOutside(event: MouseEvent | TouchEvent) {
if (mode === ClickOutsideMode.dom) {
const clickedOnAtLeastOneRef = refs
.filter((ref) => !!ref.current)
.some((ref) => ref.current?.contains(event.target as Node));
if (!clickedOnAtLeastOneRef) {
callback(event);
}
}
if (mode === ClickOutsideMode.absolute) {
const clickedOnAtLeastOneRef = refs
.filter((ref) => !!ref.current)
.some((ref) => {
if (!ref.current) {
return false;
}
const { x, y, width, height } = ref.current.getBoundingClientRect();
const clientX =
'clientX' in event
? event.clientX
: event.changedTouches[0].clientX;
const clientY =
'clientY' in event
? event.clientY
: event.changedTouches[0].clientY;
if (
clientX < x ||
clientX > x + width ||
clientY < y ||
clientY > y + height
) {
return false;
}
return true;
});
if (!clickedOnAtLeastOneRef) {
callback(event);
}
}
}
document.addEventListener('click', handleClickOutside, { capture: true });
document.addEventListener('touchend', handleClickOutside, {
capture: true,
});
return () => {
document.removeEventListener('click', handleClickOutside, {
capture: true,
});
document.removeEventListener('touchend', handleClickOutside, {
capture: true,
});
};
}, [refs, callback, mode]);
}

View File

@ -0,0 +1,42 @@
import { Profiler } from 'react';
import { Interaction } from 'scheduler/tracing';
type OwnProps = {
id: string;
children: React.ReactNode;
};
export function TimingProfiler({ id, children }: OwnProps) {
function handleRender(
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number,
interactions: Set<Interaction>,
) {
console.debug(
'TimingProfiler',
JSON.stringify(
{
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
interactions,
},
null,
2,
),
);
}
return (
<Profiler id={id} onRender={handleRender}>
{children}
</Profiler>
);
}

View File

@ -0,0 +1,23 @@
import { AppHotkeyScope } from '../types/AppHotkeyScope';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
import { HotkeyScope } from '../types/HotkeyScope';
export const INITIAL_HOTKEYS_SCOPES: string[] = [AppHotkeyScope.App];
export const ALWAYS_ON_HOTKEYS_SCOPES: string[] = [
AppHotkeyScope.CommandMenu,
AppHotkeyScope.App,
];
export const DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES: CustomHotkeyScopes = {
commandMenu: true,
goto: false,
};
export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
scope: AppHotkeyScope.App,
customScopes: {
commandMenu: true,
goto: true,
},
};

View File

@ -0,0 +1,103 @@
import { useHotkeysContext } from 'react-hotkeys-hook';
import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '../../states/internal/internalHotkeysEnabledScopesState';
export function useHotkeyScopes() {
const { disableScope, enableScope } = useHotkeysContext();
const disableAllHotkeyScopes = useRecoilCallback(
({ set, snapshot }) => {
return async () => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
for (const enabledScope of enabledScopes) {
disableScope(enabledScope);
}
set(internalHotkeysEnabledScopesState, []);
};
},
[disableScope],
);
const enableHotkeyScope = useRecoilCallback(
({ set, snapshot }) => {
return async (scopeToEnable: string) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
if (!enabledScopes.includes(scopeToEnable)) {
enableScope(scopeToEnable);
set(internalHotkeysEnabledScopesState, [
...enabledScopes,
scopeToEnable,
]);
}
};
},
[enableScope],
);
const disableHotkeyScope = useRecoilCallback(
({ set, snapshot }) => {
return async (scopeToDisable: string) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
const scopeToRemoveIndex = enabledScopes.findIndex(
(scope) => scope === scopeToDisable,
);
if (scopeToRemoveIndex > -1) {
disableScope(scopeToDisable);
enabledScopes.splice(scopeToRemoveIndex);
set(internalHotkeysEnabledScopesState, enabledScopes);
}
};
},
[disableScope],
);
const setHotkeyScopes = useRecoilCallback(
({ set, snapshot }) => {
return async (scopesToSet: string[]) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
const scopesToDisable = enabledScopes.filter(
(enabledScope) => !scopesToSet.includes(enabledScope),
);
const scopesToEnable = scopesToSet.filter(
(scopeToSet) => !enabledScopes.includes(scopeToSet),
);
for (const scopeToDisable of scopesToDisable) {
disableScope(scopeToDisable);
}
for (const scopeToEnable of scopesToEnable) {
enableScope(scopeToEnable);
}
set(internalHotkeysEnabledScopesState, scopesToSet);
};
},
[disableScope, enableScope],
);
return {
disableAllHotkeyScopes,
enableHotkeyScope,
disableHotkeyScope,
setHotkeyScopes,
};
}

View File

@ -0,0 +1,25 @@
import { Keys } from 'react-hotkeys-hook/dist/types';
import { useNavigate } from 'react-router-dom';
import { AppHotkeyScope } from '../types/AppHotkeyScope';
import { useSequenceHotkeys } from './useSequenceScopedHotkeys';
export function useGoToHotkeys(key: Keys, location: string) {
const navigate = useNavigate();
useSequenceHotkeys(
'g',
key,
() => {
navigate(location);
},
AppHotkeyScope.Goto,
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[navigate],
);
}

View File

@ -0,0 +1,43 @@
import { useState } from 'react';
import { useRecoilCallback } from 'recoil';
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
import { HotkeyScope } from '../types/HotkeyScope';
import { useSetHotkeyScope } from './useSetHotkeyScope';
export function usePreviousHotkeyScope() {
const [previousHotkeyScope, setPreviousHotkeyScope] =
useState<HotkeyScope | null>();
const setHotkeyScope = useSetHotkeyScope();
function goBackToPreviousHotkeyScope() {
if (previousHotkeyScope) {
setHotkeyScope(
previousHotkeyScope.scope,
previousHotkeyScope.customScopes,
);
}
}
const setHotkeyScopeAndMemorizePreviousScope = useRecoilCallback(
({ snapshot }) =>
(scope: string, customScopes?: CustomHotkeyScopes) => {
const currentHotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.valueOrThrow();
setHotkeyScope(scope, customScopes);
setPreviousHotkeyScope(currentHotkeyScope);
},
[setPreviousHotkeyScope],
);
return {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
};
}

View File

@ -0,0 +1,64 @@
import { Hotkey } from 'react-hotkeys-hook/dist/types';
import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
const DEBUG_HOTKEY_SCOPE = true;
export function useScopedHotkeyCallback() {
return useRecoilCallback(
({ snapshot }) =>
({
callback,
hotkeysEvent,
keyboardEvent,
scope,
preventDefault = true,
}: {
keyboardEvent: KeyboardEvent;
hotkeysEvent: Hotkey;
callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void;
scope: string;
preventDefault?: boolean;
}) => {
const currentHotkeyScopes = snapshot
.getLoadable(internalHotkeysEnabledScopesState)
.valueOrThrow();
if (!currentHotkeyScopes.includes(scope)) {
if (DEBUG_HOTKEY_SCOPE) {
console.debug(
`%cI can't call hotkey (${
hotkeysEvent.keys
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
', ',
)}]`,
'color: gray; ',
);
}
return;
}
if (DEBUG_HOTKEY_SCOPE) {
console.debug(
`%cI can call hotkey (${
hotkeysEvent.keys
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
', ',
)}]`,
'color: green;',
);
}
if (preventDefault) {
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
keyboardEvent.stopImmediatePropagation();
}
return callback(keyboardEvent, hotkeysEvent);
},
[],
);
}

View File

@ -0,0 +1,52 @@
import { useHotkeys } from 'react-hotkeys-hook';
import {
HotkeyCallback,
Keys,
Options,
OptionsOrDependencyArray,
} from 'react-hotkeys-hook/dist/types';
import { useRecoilState } from 'recoil';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
export function useScopedHotkeys(
keys: Keys,
callback: HotkeyCallback,
scope: string,
dependencies?: OptionsOrDependencyArray,
options: Options = {
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
const callScopedHotkeyCallback = useScopedHotkeyCallback();
return useHotkeys(
keys,
(keyboardEvent, hotkeysEvent) => {
callScopedHotkeyCallback({
keyboardEvent,
hotkeysEvent,
callback: () => {
if (!pendingHotkey) {
callback(keyboardEvent, hotkeysEvent);
return;
}
setPendingHotkey(null);
},
scope,
preventDefault: !!options.preventDefault,
});
},
{
enableOnContentEditable: options.enableOnContentEditable,
enableOnFormTags: options.enableOnFormTags,
},
dependencies,
);
}

View File

@ -0,0 +1,76 @@
import { Options, useHotkeys } from 'react-hotkeys-hook';
import { Keys } from 'react-hotkeys-hook/dist/types';
import { useRecoilState } from 'recoil';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
export function useSequenceHotkeys(
firstKey: Keys,
secondKey: Keys,
sequenceCallback: () => void,
scope: string,
options: Options = {
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
deps: any[] = [],
) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
const callScopedHotkeyCallback = useScopedHotkeyCallback();
useHotkeys(
firstKey,
(keyboardEvent, hotkeysEvent) => {
callScopedHotkeyCallback({
keyboardEvent,
hotkeysEvent,
callback: () => {
setPendingHotkey(firstKey);
},
scope,
preventDefault: !!options.preventDefault,
});
},
{
enableOnContentEditable: options.enableOnContentEditable,
enableOnFormTags: options.enableOnFormTags,
},
[setPendingHotkey, scope],
);
useHotkeys(
secondKey,
(keyboardEvent, hotkeysEvent) => {
callScopedHotkeyCallback({
keyboardEvent,
hotkeysEvent,
callback: () => {
if (pendingHotkey !== firstKey) {
return;
}
setPendingHotkey(null);
if (!!options.preventDefault) {
keyboardEvent.stopImmediatePropagation();
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
}
sequenceCallback();
},
scope,
preventDefault: false,
});
},
{
enableOnContentEditable: options.enableOnContentEditable,
enableOnFormTags: options.enableOnFormTags,
},
[pendingHotkey, setPendingHotkey, scope, ...deps],
);
}

View File

@ -0,0 +1,76 @@
import { useRecoilCallback } from 'recoil';
import { isDefined } from '~/utils/isDefined';
import { DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES } from '../constants';
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
import { AppHotkeyScope } from '../types/AppHotkeyScope';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
import { HotkeyScope } from '../types/HotkeyScope';
function isCustomScopesEqual(
customScopesA: CustomHotkeyScopes | undefined,
customScopesB: CustomHotkeyScopes | undefined,
) {
return (
customScopesA?.commandMenu === customScopesB?.commandMenu &&
customScopesA?.goto === customScopesB?.goto
);
}
export function useSetHotkeyScope() {
return useRecoilCallback(
({ snapshot, set }) =>
async (hotkeyScopeToSet: string, customScopes?: CustomHotkeyScopes) => {
const currentHotkeyScope = await snapshot.getPromise(
currentHotkeyScopeState,
);
if (currentHotkeyScope.scope === hotkeyScopeToSet) {
if (!isDefined(customScopes)) {
if (
isCustomScopesEqual(
currentHotkeyScope?.customScopes,
DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES,
)
) {
return;
}
} else {
if (
isCustomScopesEqual(
currentHotkeyScope?.customScopes,
customScopes,
)
) {
return;
}
}
}
const newHotkeyScope: HotkeyScope = {
scope: hotkeyScopeToSet,
customScopes: {
commandMenu: customScopes?.commandMenu ?? true,
goto: customScopes?.goto ?? false,
},
};
const scopesToSet: string[] = [];
if (newHotkeyScope.customScopes?.commandMenu) {
scopesToSet.push(AppHotkeyScope.CommandMenu);
}
if (newHotkeyScope?.customScopes?.goto) {
scopesToSet.push(AppHotkeyScope.Goto);
}
scopesToSet.push(newHotkeyScope.scope);
set(internalHotkeysEnabledScopesState, scopesToSet);
},
[],
);
}

View File

@ -0,0 +1,9 @@
import { atom } from 'recoil';
import { INITIAL_HOTKEYS_SCOPE } from '../../constants';
import { HotkeyScope } from '../../types/HotkeyScope';
export const currentHotkeyScopeState = atom<HotkeyScope>({
key: 'currentHotkeyScopeState',
default: INITIAL_HOTKEYS_SCOPE,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const internalHotkeysEnabledScopesState = atom<string[]>({
key: 'internalHotkeysEnabledScopesState',
default: [],
});

View File

@ -0,0 +1,7 @@
import { Keys } from 'react-hotkeys-hook/dist/types';
import { atom } from 'recoil';
export const pendingHotkeyState = atom<Keys | null>({
key: 'pendingHotkeyState',
default: null,
});

View File

@ -0,0 +1,5 @@
export enum AppHotkeyScope {
App = 'app',
Goto = 'goto',
CommandMenu = 'command-menu',
}

View File

@ -0,0 +1,4 @@
export type CustomHotkeyScopes = {
goto?: boolean;
commandMenu?: boolean;
};

View File

@ -0,0 +1,6 @@
import { CustomHotkeyScopes } from './CustomHotkeyScope';
export type HotkeyScope = {
scope: string;
customScopes?: CustomHotkeyScopes;
};

View File

@ -0,0 +1,57 @@
export function isNonTextWritingKey(key: string) {
const nonTextWritingKeys = [
'Enter',
'Tab',
'Shift',
'Escape',
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'Delete',
'Backspace',
'F1',
'F2',
'F3',
'F4',
'F5',
'F6',
'F7',
'F8',
'F9',
'F10',
'F11',
'F12',
'Meta',
'Alt',
'Control',
'CapsLock',
'NumLock',
'ScrollLock',
'Pause',
'Insert',
'Home',
'PageUp',
'Delete',
'End',
'PageDown',
'ContextMenu',
'PrintScreen',
'BrowserBack',
'BrowserForward',
'BrowserRefresh',
'BrowserStop',
'BrowserSearch',
'BrowserFavorites',
'BrowserHome',
'VolumeMute',
'VolumeDown',
'VolumeUp',
'MediaTrackNext',
'MediaTrackPrevious',
'MediaStop',
'MediaPlayPause',
];
return nonTextWritingKeys.includes(key);
}

View File

@ -0,0 +1,8 @@
import { HotkeyScope } from '../types/HotkeyScope';
export function isSameHotkeyScope(
hotkeyScope1: HotkeyScope | undefined | null,
hotkeyScope2: HotkeyScope | undefined | null,
): boolean {
return JSON.stringify(hotkeyScope1) === JSON.stringify(hotkeyScope2);
}

View File

@ -0,0 +1,12 @@
import { useLocation } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { currentPageLocationState } from '../states/currentPageLocationState';
export function useIsPageLoading() {
const currentLocation = useLocation().pathname;
const currentPageLocation = useRecoilValue(currentPageLocationState);
return currentLocation !== currentPageLocation;
}

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const currentPageLocationState = atom<string>({
key: 'currentPageLocationState',
default: '',
});

View File

@ -0,0 +1,24 @@
import { Context, useRef } from 'react';
import { v4 } from 'uuid';
import { RecoilScopeContext } from '../states/RecoilScopeContext';
export function RecoilScope({
SpecificContext,
children,
}: {
SpecificContext?: Context<string | null>;
children: React.ReactNode;
}) {
const currentScopeId = useRef(v4());
return SpecificContext ? (
<SpecificContext.Provider value={currentScopeId.current}>
{children}
</SpecificContext.Provider>
) : (
<RecoilScopeContext.Provider value={currentScopeId.current}>
{children}
</RecoilScopeContext.Provider>
);
}

View File

@ -0,0 +1,12 @@
import { Context, useContext } from 'react';
export function useContextScopeId(SpecificContext: Context<string | null>) {
const recoilScopeId = useContext(SpecificContext);
if (!recoilScopeId)
throw new Error(
`Using useContextScopedId outside of the specified context : ${SpecificContext.displayName}, verify that you are using a RecoilScope with the specific context you want to use.`,
);
return recoilScopeId;
}

View File

@ -0,0 +1,20 @@
import { Context, useContext } from 'react';
import { RecoilState, useRecoilState } from 'recoil';
import { RecoilScopeContext } from '../states/RecoilScopeContext';
export function useRecoilScopedState<StateType>(
recoilState: (param: string) => RecoilState<StateType>,
SpecificContext?: Context<string | null>,
) {
const recoilScopeId = useContext(SpecificContext ?? RecoilScopeContext);
if (!recoilScopeId)
throw new Error(
`Using a scoped atom without a RecoilScope : ${
recoilState('').key
}, verify that you are using a RecoilScope with a specific context if you intended to do so.`,
);
return useRecoilState<StateType>(recoilState(recoilScopeId));
}

View File

@ -0,0 +1,20 @@
import { Context, useContext } from 'react';
import { RecoilState, useRecoilValue } from 'recoil';
import { RecoilScopeContext } from '../states/RecoilScopeContext';
export function useRecoilScopedValue<T>(
recoilState: (param: string) => RecoilState<T>,
SpecificContext?: Context<string | null>,
) {
const recoilScopeId = useContext(SpecificContext ?? RecoilScopeContext);
if (!recoilScopeId)
throw new Error(
`Using a scoped atom without a RecoilScope : ${
recoilState('').key
}, verify that you are using a RecoilScope with a specific context if you intended to do so.`,
);
return useRecoilValue<T>(recoilState(recoilScopeId));
}

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const RecoilScopeContext = createContext<string | null>(null);

View File

@ -0,0 +1,13 @@
import { RecoilState, Snapshot } from 'recoil';
export function getSnapshotScopedState<T>({
snapshot,
state,
contextScopeId,
}: {
snapshot: Snapshot;
state: (scopeId: string) => RecoilState<T>;
contextScopeId: string;
}) {
return snapshot.getLoadable(state(contextScopeId)).valueOrThrow();
}

View File

@ -0,0 +1,11 @@
import { RecoilState, Snapshot } from 'recoil';
export function getSnapshotState<T>({
snapshot,
state,
}: {
snapshot: Snapshot;
state: RecoilState<T>;
}) {
return snapshot.getLoadable(state).valueOrThrow();
}

View File

@ -0,0 +1,7 @@
import { useMediaQuery } from 'react-responsive';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
export function useIsMobile() {
return useMediaQuery({ query: `(max-width: ${MOBILE_VIEWPORT}px)` });
}