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 {
computed,
ComputedGetter,
ComputedRef,
readonly,
Ref,
ref,
watch,
watchEffect,
} from 'vue'
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)

Konrad Mohrfeldt
committed
async function wrapper(...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> {
return (await fn(...args)) as Awaited<ReturnType<F>>

Konrad Mohrfeldt
committed
return { fn: wrapper, isProcessing }
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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 secondsToDurationString(seconds: number): string {

Konrad Mohrfeldt
committed
const isNegative = seconds < 0
const _seconds = Math.abs(Math.round(seconds))
if (_seconds === 0) return `0s`
const h = Math.floor(_seconds / 3600)
const m = Math.floor((_seconds % 3600) / 60)
const s = Math.round(_seconds % 60)
const representation = `${h ? h + 'h ' : ''}${m ? m + 'min ' : ''}${s ? s + 's' : ''}`.trim()
return isNegative ? `-${representation}` : representation
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()
)
})
}

Konrad Mohrfeldt
committed
export function areSetsEqual<T extends string | number | symbol>(
firstIterable: Iterable<T>,
secondIterable: Iterable<T>,
): boolean {
const set1 = new Set(firstIterable)
const set2 = new Set(secondIterable)
if (set1.size !== set2.size) return false
for (const item of set1) {
if (!set2.has(item)) return false
}
return true
}
export function getFilenameFromURL(url: string, defaultValue?: string) {
const basename = (url.split(/[\\/]/).pop() as string).trim()
return !basename && defaultValue ? defaultValue : basename
}
function arrayPadLeft<T>(array: T[], maxLength: number, fillValue: T): T[] {
return Array(Math.max(0, maxLength - array.length))
.fill(fillValue)
.concat(array)
}
export function parseTime(time: string): number {
let [days, hours, minutes, seconds] = Array(4).fill(0)
if (time.includes(':')) {
;[days, hours, minutes, seconds] = arrayPadLeft<number | string>(time.split(':'), 4, 0)
} else {
// parses strings like: "3m32", "3 h 2s ", "9d2h3min1"
const format =
/^\s*(?:(?<days>\d+)\s*d)?\s*(?:(?<hours>\d+)\s*h)?\s*(?:(?<minutes>\d+)\s*m(?:in)?)?\s*(?:(?<seconds>\d+)\s*s?)?\s*$/
;({ days, hours, minutes, seconds } = time.match(format)?.groups ?? {})
}
return (
parseInt(days || 0) * 24 * 60 * 60 +
parseInt(hours || 0) * 60 * 60 +
parseInt(minutes || 0) * 60 +
parseInt(seconds || 0)
)
}