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 { storeToRefs } from 'pinia'
import {
computed,
ComputedGetter,
ComputedRef,
readonly,
Ref,
ref,
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>>> {
return (await fn(...args)) as Awaited<ReturnType<F>>
return { fn: wrapper, isProcessing }
}
export function useUpdatableState<T>(
externalStateRef: MaybeRefOrGetter<T>,
options: { clone?: (v: T) => T; isEqual?: (v1: T, v2: T) => boolean } = {},
const _clone = options?.clone ?? cloneDeep
const _isEqual = options?.isEqual ?? isEqual
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)
},
})
}

Konrad Mohrfeldt
committed
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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 { 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()
)
})
}