Skip to content
Snippets Groups Projects
util.ts 8.4 KiB
Newer Older
import { computedAsync, useNow } from '@vueuse/core'
import { formatDistanceToNow, formatISO, parseISO } from 'date-fns'
import DateFnEnUsLocale from 'date-fns/locale/en-US'
import DOMPurify from 'dompurify'
import { cloneDeep, isEqual } from 'lodash'
import { storeToRefs } from 'pinia'
import {
  computed,
  ComputedGetter,
  ComputedRef,
  MaybeRefOrGetter,
  shallowReadonly,
  watch,
  watchEffect,
} from 'vue'
import { useI18n } from '@/i18n'
import { SteeringUser } from '@/stores/auth'
import { useShowStore } from '@/stores'

const dateFnLocales = {
  de: () => import('date-fns/locale/de'),
}

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: MaybeRefOrGetter<T>,
  onUpdate: (value: T) => void,
  options: { clone?: (v: T) => T; isEqual?: (v1: T, v2: T) => boolean } = {},
): Ref<T> {
  const _clone = options?.clone ?? cloneDeep
  const _isEqual = options?.isEqual ?? isEqual

Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
  const localRef = ref() as Ref<T>
  watchEffect(() => {
    localRef.value = _clone(toValue(externalStateRef))
  })
  watch(
    localRef,
    (newValue) => {
      if (!_isEqual(newValue, toValue(externalStateRef))) {
        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 ensureDate(date: Date | string) {
  return typeof date === 'string' ? parseISO(date) : date
}

export function stripSecondsFromTimeString(time: string) {
  return /\d+:\d+:\d+$/.test(time) ? time.replace(/:\d+$/, '') : time
}

export function calculateDurationSeconds(start: Date | string, end: Date | string): number {
  return Math.abs(ensureDate(end).getTime() - ensureDate(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: [] })
}
  const { selectedShow } = storeToRefs(useShowStore())
  return selectedShow

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)
    },
  })
}

export function matchesSearch(sourceString: string, searchString: string) {
  const source = sourceString.toLowerCase()
  const terms = searchString
    .toLowerCase()
    .split(/\s+/)
    .map((s) => s.trim())
    .filter((s) => !!s)
  for (const term of terms) {
    if (!source.includes(term)) return false
  }
  return true
}

export function computedDebounced<T>(
  value: Ref<T> | (() => T),
  delayBySeconds: MaybeRefOrGetter<number> | ((newValue: T, oldValue: T) => number),
) {
  const debouncedValue = ref<T>(toValue(value)) as Ref<T>
  let timer: ReturnType<typeof setTimeout> | undefined

  watch(toRef(value), (newValue, oldValue) => {
    if (timer) clearTimeout(timer)
    const delay =
      typeof delayBySeconds === 'function'
        ? delayBySeconds(newValue, oldValue)
        : toValue(delayBySeconds)
    if (delay === 0) {
      debouncedValue.value = newValue
    } else {
      timer = setTimeout(() => {
        debouncedValue.value = newValue
      }, delay * 1000)
    }
  })

  return shallowReadonly(debouncedValue)
}

export function useDateFnLocale(language: MaybeRefOrGetter<string>) {
  return computedAsync(async () => {
    const localeString = toValue(language) as keyof typeof dateFnLocales
    let locale: Locale = DateFnEnUsLocale
    if (localeString in dateFnLocales) {
      locale = (await dateFnLocales[localeString]()).default
    }
    return locale
  }, DateFnEnUsLocale)
}

export function useRelativeDistanceToNow(
  date: MaybeRefOrGetter<Date | string | null | undefined>,
  options: { includeSeconds?: boolean; addSuffix?: boolean } = {},
) {
  const { locale: appLocale } = useI18n()
  const dateFnLocale = useDateFnLocale(appLocale)
  return computedAsync(async () => {
    const dateValue = toValue(date)
    if (!dateValue) return ''
    return formatDistanceToNow(ensureDate(dateValue), { locale: dateFnLocale.value, ...options })

export function usePersonName(user: MaybeRefOrGetter<SteeringUser>) {
  return computed(() => {
    const _user = toValue(user)
    const firstName = (_user?.firstName ?? '').trim()
    const lastName = (_user.lastName ?? '').trim()
    const username = _user.username.trim()
    const name = [firstName, lastName].join(' ')
    const initials =
      firstName && lastName
        ? [firstName[0], lastName[0]]
        : lastName
        ? [lastName[0]]
        : firstName
        ? [firstName[0]]
        : [username[0]]
    return {
      name,
      username,
      initials: initials.join('').toUpperCase(),
    }
  })
}

export function useIsToday(
  date: MaybeRefOrGetter<Date | string>,
  today?: MaybeRefOrGetter<Date | string>,
) {
  const datetime = computed(() => ensureDate(toValue(date)))
  const now =
    typeof today === 'undefined'
      ? useNow({ interval: 60 * 1000 })
      : computed(() => ensureDate(toValue(today)))
  return computed(() => {
    return (
      now.value.getDate() === datetime.value.getDate() &&
      now.value.getMonth() === datetime.value.getMonth() &&
      now.value.getFullYear() === datetime.value.getFullYear()
    )
  })
}