import { computedAsync } 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 { computed, ComputedGetter, ComputedRef, MaybeRefOrGetter, readonly, Ref, ref, shallowReadonly, shallowRef, toRef, toValue, watch, watchEffect, } from 'vue' import { useStore } from 'vuex' import { Show } from '@/types' import { useI18n } from '@/i18n' import { SteeringUser } from '@/stores/auth' 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 try { 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() as Ref<T> 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) }, }) } 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 | 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(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(), } }) }