Skip to content
Snippets Groups Projects
util.ts 4.75 KiB
Newer Older
  • Learn to ignore specific revisions
  • import { cloneDeep, isEqual } from 'lodash'
    
    import {
      computed,
      ComputedGetter,
      ComputedRef,
      readonly,
      Ref,
      ref,
      shallowRef,
      watch,
      watchEffect,
    } from 'vue'
    
    import { formatISO, parseISO } from 'date-fns'
    
    import DOMPurify from 'dompurify'
    
    import { useStore } from 'vuex'
    import { Show } from '@/types'
    
    
    export function computedIter<T>(fn: ComputedGetter<Iterable<T>>): ComputedRef<T[]> {
      return computed(() => Array.from(fn()))
    }
    
    
    export const useId = (() => {
      let _id = 0
      return function useId(prefix = 'component') {
        return readonly(ref(`${prefix}-${_id++}`))
      }
    })()
    
    export function useAsyncFunction<F extends (...args: never[]) => Promise<unknown>>(fn: F) {
    
      const isProcessing = ref(false)
    
      async function wrapper(...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> {
    
        isProcessing.value = true
    
          return (await fn(...args)) as Awaited<ReturnType<F>>
    
        } finally {
    
          isProcessing.value = false
    
      return { fn: wrapper, isProcessing }
    
    }
    
    export function useUpdatableState<T>(
      externalStateRef: Ref<T>,
      onUpdate: (value: T) => void,
      clone: (value: T) => T = cloneDeep,
    ): Ref<T> {
      const localRef = ref()
      watchEffect(() => {
        localRef.value = clone(externalStateRef.value)
      })
      watch(
        localRef,
        (newValue) => {
          if (!isEqual(newValue, externalStateRef.value)) {
            onUpdate(newValue)
          }
        },
        { deep: true },
      )
      return localRef
    }
    
    export function useCopy<T, R = T>(externalStateRef: Ref<T>, transform?: (value: T) => R) {
      const _transform = transform ?? ((v: T) => v as unknown as R)
      const localRef = shallowRef<R>(_transform(externalStateRef.value))
      watch(externalStateRef, (newValue) => {
        localRef.value = _transform(newValue)
      })
      return localRef
    }
    
    
    export function useFormattedISODate(date: Ref<Date>) {
      return computed({
        get() {
          return formatISO(date.value, { representation: 'date' })
        },
        set(dateValue: string) {
          date.value = parseISO(dateValue)
        },
      })
    }
    
    
    export function getClosestSlot(slotDurationMinutes: number, date?: Date): Date {
      date = date ?? new Date()
      const slotDurationMillis = slotDurationMinutes * 60 * 1000
      const slots = Math.floor(date.getTime() / slotDurationMillis)
      const closestSlotTimestamp = slots * slotDurationMillis
      return new Date(closestSlotTimestamp)
    }
    
    export function getNextAvailableSlot(slotDurationMinutes: number, date?: Date): Date {
      date = date ?? new Date()
      const slotDurationMillis = slotDurationMinutes * 60 * 1000
      const slots = Math.ceil(date.getTime() / slotDurationMillis)
      const nextSlotTimestamp = slots * slotDurationMillis
      return new Date(nextSlotTimestamp)
    }
    
    export function calculateDurationSeconds(start: Date, end: Date): number {
      return Math.abs(end.getTime() - start.getTime()) / 1000
    }
    
    
    export function sanitizeHTML(
      html: string,
      preset?: 'inline-noninteractive' | 'safe-html' | 'strip',
    ): string {
      const inlineElements = [
        'b',
        'big',
        'i',
        'small',
        'tt',
        'abbr',
        'acronym',
        'cite',
        'code',
        'dfn',
        'em',
        'kbd',
        'strong',
        'samp',
        'var',
        'br',
        'q',
        's',
        'span',
        'sub',
        'sup',
      ]
    
      if (preset === 'inline-noninteractive') {
        return DOMPurify.sanitize(html, {
          ALLOWED_TAGS: inlineElements,
          ALLOWED_ATTR: ['style', 'title'],
        })
      }
    
      if (preset === 'safe-html') {
        return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } })
      }
    
      return DOMPurify.sanitize(html, { ALLOWED_TAGS: [] })
    }
    
    
    export function useSelectedShow() {
      const store = useStore()
      return computed<Show>({
        get() {
          return store.state.shows.shows[store.state.shows.selected.index]
        },
        set(show) {
          store.commit('shows/switchShowById', show.id)
        },
      })
    }
    
    
    export function secondsToDurationString(seconds: number): string {
    
      seconds = Math.round(seconds)
    
      const h = Math.floor(seconds / 3600)
      const m = Math.floor((seconds % 3600) / 60)
      const s = Math.round(seconds % 60)
      return `${h ? h + 'h ' : ''}${m ? m + 'min ' : ''}${s ? s + 's' : ''}`
    }
    
    
    export function clamp(value: number, min: number, max: number): number {
      return Math.min(Math.max(value, min), max)
    }
    
    export function mapToDomain(
      value: number,
      inputDomain: [number, number],
      outputDomain: [number, number],
    ) {
      const [x1, y1] = inputDomain
      const [x2, y2] = outputDomain
      value = clamp(value, x1, y1)
      return ((value - x1) * (y2 - x2)) / (y1 - x1) + x2
    }
    
    
    export function asyncWritableComputed<T>(
      initialValue: T,
      config: {
        get: () => Promise<T>
        set: (value: T) => Promise<void> | void
      },
    ) {
      const data = shallowRef(initialValue)
    
      watchEffect(async () => {
        data.value = await config.get()
      })
    
      return computed<T>({
        get: () => data.value,
        set: (value: T) => {
          config.set(value)
        },
      })
    }