All files / app/src/shared/lib/jotai factories.ts

67.85% Statements 76/112
42.85% Branches 12/28
66.66% Functions 8/12
67.85% Lines 76/112

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 1601x                                           1x 30881x   30881x 31218x   30881x   30881x 30881x     30881x 1x 1x   1x   1x   1x   1x   1x   1x   1x 1x 1x 1x   30881x 30881x               30881x 30881x 31217x     31217x 31217x   31217x 31217x 31217x     31217x 30881x 30881x 30881x 336x                     336x 336x 30881x 30881x   30881x 30881x   1x 30881x 30881x 30881x 30881x 30881x   30881x       30881x 30881x 30881x 30881x 30881x 30881x 30881x 336x 336x 336x 336x 336x 336x 336x 30881x 30881x 30881x 30881x 30881x   1x 2156x 2156x   2156x 2156x 2156x       1x             1x                      
import { isTestEnv } from '@/shared/config'
import { Nullable } from '@tmk/ui-kit'
import { SetStateAction, WritableAtom } from 'jotai'
import { atomWithReset, atomWithStorage, RESET } from 'jotai/utils'
import Router from 'next/router'
 
export type StorageType = 'hash' | 'query' | 'none'
 
export interface FilterAtomParams {
  name: string
  initialValue: unknown
}
 
export interface AtomWithHashOptions<Value> {
  storageType?: StorageType
  serialize?: (val: Value) => string
  deserialize?: (str: string | null) => Value
  delayInit?: boolean
  replaceState?: boolean
  subscribe?: (callback: () => void) => () => void
}
 
export const atomWithUrlLocation = <Value>(key: string, initialValue: Value, options: AtomWithHashOptions<Value>) => {
  const isStorageTypeHash = options?.storageType === 'hash'
 
  const getURLParams = () =>
    isStorageTypeHash ? new URLSearchParams(location.hash.slice(1)) : new URLSearchParams(location.search.slice(1))
 
  const urlMark = isStorageTypeHash ? '#' : '?'
 
  const serialize = options?.serialize || JSON.stringify
  const deserialize = options?.deserialize || JSON.parse
 
  function updateQuery(key: string): void
  function updateQuery(key: string, newValue?: Value): void {
    const searchParams = getURLParams()
    newValue ? searchParams.set(key, serialize(newValue)) : searchParams.delete(key)
 
    const currentState = history.state
 
    const searchParamsString = searchParams.toString()
 
    const as = isStorageTypeHash
      ? currentState.as
      : history.state?.as?.replace(/(#|\?)(.+)?/gm, '') + (searchParamsString ? urlMark + searchParamsString : '')
 
    const historyState = { ...currentState, as, key: key + Date.now() }
 
    if (options?.replaceState || history.state?.as === historyState.as) {
      history.replaceState(historyState, '', as)
    } else {
      history.pushState(historyState, '', as)
    }
  }
 
  const subscribe =
    options?.subscribe ||
    (callback => {
      window.addEventListener('popstate', callback)
      return () => {
        window.removeEventListener('popstate', callback)
      }
    })
 
  const queryStorage = {
    getItem: (key: string) => {
      if (typeof window === 'undefined') {
        return initialValue
      }
      const searchParams = getURLParams()
      const storedValue = searchParams.get(key)
 
      if (storedValue === null) {
        return initialValue
      }
 
      return deserialize(storedValue)
    },
    setItem: updateQuery,
    removeItem: updateQuery,
    subscribe: (key: string, setValue: (value: Value) => void) => {
      const callback = () => {
        const searchParams = getURLParams()
        const parameter = searchParams.get(key)
 
        if (parameter !== null) {
          setValue(deserialize(parameter))
        } else {
          setValue(initialValue)
        }
      }
 
      return subscribe(callback)
    },
    ...(options?.delayInit && { delayInit: true }),
  }
 
  return atomWithStorage(key, initialValue, queryStorage)
}
 
export const atomWithStorageFactory = <Value>(
  key: string,
  initialValue: Value,
  options?: AtomWithHashOptions<Value>
): WritableAtom<Value, SetStateAction<Value> | typeof RESET> => {
  const eventName = options?.storageType === 'hash' ? 'hashchange' : 'popstate'
 
  if (options?.storageType === 'none') {
    return atomWithReset(initialValue)
  }
 
  return atomWithUrlLocation(
    key,
    initialValue,
    Object.assign(
      {
        delayInit: false,
        subscribe: callback => {
          isTestEnv || Router.events.on('routeChangeComplete', callback)
          window.addEventListener(eventName, callback)
          return () => {
            isTestEnv || Router.events.off('routeChangeComplete', callback)
            window.removeEventListener(eventName, callback)
          }
        },
      } as AtomWithHashOptions<Value>,
      options
    )
  )
}
 
export const filterAtomsFactory = <FiltersContent extends Record<string, FiltersContent[keyof FiltersContent]>>(
  filters: FiltersContent,
  options?: AtomWithHashOptions<FiltersContent[keyof FiltersContent]>
) =>
  Object.fromEntries(
    Object.entries(filters).map(([key, value]) => [key, atomWithStorageFactory(key, value, options)])
  ) as {
    [key in keyof FiltersContent]: WritableAtom<FiltersContent[key], SetStateAction<FiltersContent[key]> | typeof RESET>
  }
 
export const getNextOrderSort = (prevValue: Nullable<string>, defaultSort: unknown): Nullable<string> => {
  if (prevValue === null) return defaultSort as Nullable<string>
  if (prevValue?.includes('desc')) return prevValue.replace('desc', 'asc') as Nullable<string>
  if (prevValue?.includes('asc')) return null as Nullable<string>
  return null
}
 
export const getTwoStatesSort = (
  sort: Nullable<string>,
  defaultSort: string,
  nextSort: string,
  key = 'order[date]'
) => {
  const currentSort = JSON.parse(sort as string)[key]
  if (currentSort === 'asc') return defaultSort
  if (currentSort === 'desc') return nextSort
  return null
}