import { cloneDeep, isEqual } from 'lodash' import { computed, ComputedGetter, ComputedRef, readonly, Ref, ref, 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 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) }, }) }