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 }
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
type UseFormattedISODateOptions = {
representation?: 'date' | 'time' | 'complete' | undefined
stripOffset?: boolean | undefined
}
export function useFormattedISODate(
date: Ref<Date>,
defaultValue?: MaybeRefOrGetter<Date>,
options?: UseFormattedISODateOptions | undefined,
): ComputedRef<string>
export function useFormattedISODate(
date: Ref<Date | null | undefined>,
defaultValue?: MaybeRefOrGetter<Date>,
options?: UseFormattedISODateOptions | undefined,
): ComputedRef<string | undefined>
export function useFormattedISODate(
date: Ref<Date | null | undefined>,
defaultValue?: MaybeRefOrGetter<Date> | undefined,
options?: UseFormattedISODateOptions | undefined,
) {
const offsetPattern = /(Z|\+\d\d:\d\d)$/
function maybeStripOffset(isoDate: string) {
return options?.stripOffset ? isoDate.replace(offsetPattern, '') : isoDate
}
function maybeAddOffset(isoDate: string, currentDate: Date | undefined) {
if (!options?.stripOffset) return isoDate
const match = isoDate.match(offsetPattern)
if (match) return isoDate
const _currentDate = currentDate ?? new Date()
const offset = (formatISO(_currentDate).match(offsetPattern) as RegExpMatchArray)[0]
return isoDate + offset
}
return computed({
get() {
return date.value
? maybeStripOffset(
formatISO(date.value, { representation: options?.representation ?? 'complete' }),
)
: null
set(dateValue: string | null | undefined) {
const _defaultValue = toValue(defaultValue)
const offsetRefDate = date.value ?? _defaultValue
date.value = dateValue
? parseISO(maybeAddOffset(dateValue, offsetRefDate))
: _defaultValue ?? null

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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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}`
}
export function useQuery(
query: MaybeRefOrGetter<Record<string, string | number | null | undefined>>,
) {
return computed(() => {
const _query: Record<string, string> = {}
for (const [key, value] of Object.entries(toValue(query))) {
if (value === null || value === undefined) continue
_query[key] = value.toString()
}
return new URLSearchParams(_query)
})
}