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