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)
const error = ref<Error | undefined>()

Konrad Mohrfeldt
committed
async function wrapper(...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> {
error.value = undefined
return (await fn(...args)) as Awaited<ReturnType<F>>
} catch (e) {
if (e instanceof Error) {
error.value = e
} else {
error.value = new Error(String(e))
}
throw error.value
return { fn: wrapper, error, 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
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
133
134
135
136
137
138
139
140
141
142
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)
)
}
export function useForeignSlotNames() {
const slots = useSlots()
return computed(() => {
return Object.entries(slots)
.filter(([, slot]) => slot && slot.name === '')
.map(([key]) => key)
})
}
export function humanJoin(iterable: Iterable<string>, glue: string, lastGlue: string) {
const array = Array.from(iterable)
if (array.length === 0) return ''
if (array.length === 1) return array[0]
if (array.length === 2) return array.join(lastGlue)
const lastValue = array.pop() as string
return `${array.join(glue)}${lastGlue}${lastValue}`
}