Skip to content

Instantly share code, notes, and snippets.

@danielres
Last active November 22, 2023 15:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielres/3c714d6388c5642d39c9725d0b8dd581 to your computer and use it in GitHub Desktop.
Save danielres/3c714d6388c5642d39c9725d0b8dd581 to your computer and use it in GitHub Desktop.
Common ts utils

Common TS utils

A collection of functions in typescript that I use often in different projects.

import { describe, it, expect } from 'vitest'
import { countOccurrences } from './array'
describe('countOccurrences()', () => {
it('counts occurrences of each string', () => {
const actual = countOccurrences(['person', 'content', 'person'])
const expected = { person: 2, content: 1 }
expect(actual).toEqual(expected)
})
it('returns an empty object for an empty array', () => {
const actual = countOccurrences([])
const expected = {}
expect(actual).toEqual(expected)
})
it('handles arrays with a single string', () => {
const actual = countOccurrences(['person'])
const expected = { person: 1 }
expect(actual).toEqual(expected)
})
it('handles arrays with multiple identical strings', () => {
const actual = countOccurrences(['person', 'person', 'person'])
const expected = { person: 3 }
expect(actual).toEqual(expected)
})
})
export function onlyUnique<T>(value: T, index: number, array: T[]) {
return array.indexOf(value) === index
}
export function onlyUniqueObjects<T>(value: T, index: number, array: T[]) {
return array.map((e) => JSON.stringify(e)).indexOf(JSON.stringify(value)) === index
}
export function countOccurrences(arr: string[]): { [key: string]: number } {
return arr.reduce((acc, curr) => {
acc[curr] = (acc[curr] || 0) + 1
return acc
}, {} as { [key: string]: number })
}
export const HSLAdjustment = (
srcImageData: ImageData,
hueDelta: number,
satDelta: number,
lightness: number
) => {
const srcPixels = srcImageData.data
const dstImageData = new ImageData(srcImageData.width, srcImageData.height)
const dstPixels = dstImageData.data
for (let i = 0; i < srcPixels.length; i += 4) {
const [h, s, l] = rgbToHsl(srcPixels[i], srcPixels[i + 1], srcPixels[i + 2])
const newH = (h + hueDelta / 360) % 1
const newS = Math.min(Math.max(s + satDelta / 100, 0), 1)
const newL = l + lightness / 100
const [r, g, b] = hslToRgb(newH, newS, newL)
dstPixels[i] = clamp(r)
dstPixels[i + 1] = clamp(g)
dstPixels[i + 2] = clamp(b)
dstPixels[i + 3] = srcPixels[i + 3]
}
return dstImageData
}
export const clamp = (value: number) => Math.max(0, Math.min(255, value))
export const rgbToHsl = (r: number, g: number, b: number) => {
r /= 255
g /= 255
b /= 255
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
let h = (max + min) / 2
let s = (max + min) / 2
const l = (max + min) / 2
if (max === min) {
h = s = 0
} else {
const delta = max - min
s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min)
h =
max === r
? (g - b) / delta + (g < b ? 6 : 0)
: max === g
? (b - r) / delta + 2
: (r - g) / delta + 4
h /= 6
}
return [h, s, l]
}
export const hslToRgb = (h: number, s: number, l: number) => {
let r, g, b
if (s === 0) {
r = g = b = l
} else {
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1
if (t > 1) t -= 1
if (t < 1 / 6) return p + (q - p) * 6 * t
if (t < 1 / 2) return q
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
return p
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
const p = 2 * l - q
r = hue2rgb(p, q, h + 1 / 3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1 / 3)
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]
}
function getOrdinalSuffix(day: number) {
if (day % 10 === 1 && day !== 11) return 'st'
if (day % 10 === 2 && day !== 12) return 'nd'
if (day % 10 === 3 && day !== 13) return 'rd'
return 'th'
}
export function renderDate(date: Date) {
// prettier-ignore
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const day = date.getDate()
const month = months[date.getMonth()]
const year = date.getFullYear()
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const currentYear = new Date().getFullYear()
// If the year of the given date is not the current year, append the year to the output
const yearOutput = year !== currentYear ? ` ${year}` : ''
return `${month} ${day}${getOrdinalSuffix(day)}${yearOutput} ${hours}:${minutes}`
}
export function checkOverflow(el: HTMLElement) {
const curOverflow = el.style.overflow
if (!curOverflow || curOverflow === 'visible') el.style.overflow = 'hidden'
const isOverflowing = el.clientWidth < el.scrollWidth || el.clientHeight < el.scrollHeight
el.style.overflow = curOverflow
return isOverflowing
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const debounce = (cb: (...args: any[]) => void, delay = 250) => {
let timeout: NodeJS.Timeout
return (...args: unknown[]) => {
clearTimeout(timeout)
timeout = setTimeout(() => cb(...args), delay)
}
}
import { describe, it, expect } from 'vitest'
import { nestedify } from './form'
describe('forms.nestedify()', () => {
const input = {
targetKind: 'person',
targetId: 'targetId',
topicId: 'topicId',
text: '',
'levels.interest': '3',
'levels.expertise': '3',
'deep.nested.value': 'deepnestedvalue',
}
it('Converts an object with dot-delimited keys into a nested object structure', () => {
const actual = nestedify(input)
const expected = {
targetKind: 'person',
targetId: 'targetId',
topicId: 'topicId',
text: '',
levels: {
interest: 3,
expertise: 3,
},
deep: {
nested: {
value: 'deepnestedvalue',
},
},
}
expect(actual).toEqual(expected)
})
})
export type NestedObject = {
[key: string]: string | number | NestedObject
}
export function nestedify(obj: { [key: string]: string }, acc: NestedObject = {}): NestedObject {
return Object.keys(obj).reduce((currentObj: NestedObject, key: string) => {
const parts = key.split('.')
parts.reduce((currentObj: NestedObject, part: string, idx: number) => {
if (idx === parts.length - 1) {
// It's the last part, set the value
currentObj[part] =
obj[key] === '' ? '' : isNaN(Number(obj[key])) ? obj[key] : Number(obj[key])
} else {
// Not the last part, ensure this level of nesting exists
currentObj[part] = currentObj[part] || ({} as NestedObject)
}
return currentObj[part] as NestedObject
}, currentObj)
return currentObj
}, acc)
}
export type FormOnSubmitEvent = Event & { currentTarget: EventTarget & HTMLFormElement }
export type FormOnSubmit = (e: FormOnSubmitEvent) => void
export function getFormOnSubmitEventValues(e: FormOnSubmitEvent) {
return Object.fromEntries(new FormData(e.currentTarget))
}
export function sanitizeFormInput(str: string) {
return upperFirst(str.replace(/\s+/g, ' ').trim())
}
import type { Person, Topic, Trait, Content } from './types.d.ts'
const BASE_PATH = '/base-path'
export const paths = {
persons: (personSlug?: Person['slug']) =>
[BASE_PATH, 'persons', personSlug].filter(Boolean).join('/'),
contents: (contentSlug?: Content['slug']) =>
[BASE_PATH, 'contents', contentSlug].filter(Boolean).join('/'),
topics: (topicSlug?: Topic['slug']) => [BASE_PATH, 'topics', topicSlug].filter(Boolean).join('/'),
traits: (traitId?: Trait['id']) => [BASE_PATH, 'traits', traitId].filter(Boolean).join('/'),
resource: (resource: Trait | Topic | Person | Content) => {
if (resource.resourceType === 'trait') return paths.traits(resource.id)
if (resource.resourceType === 'topic') return paths.topics(resource.slug)
if (resource.resourceType === 'person') return paths.persons(resource.slug)
if (resource.resourceType === 'content') return paths.contents(resource.slug)
},
}
export function groupByKey<K extends keyof T, T>(list: T[], key: K): Record<K, T[]> {
return list.reduce(
(hash, obj) => ({
...hash,
[obj[key] as K]: (hash[obj[key] as K] || []).concat(obj),
}),
{} as Record<K, T[]>
)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function groupByNestedKey<T extends Record<string, any>>(
list: T[],
keyPath: string[]
): Record<string, T[]> {
const getKey = (obj: T, path: string[]) => {
return path.reduce((value, key) => (value && value[key] != null ? value[key] : null), obj)
}
return list.reduce((hash, obj) => {
const key = getKey(obj, keyPath)
const keyString = key != null ? key.toString() : ''
if (!hash[keyString]) {
hash[keyString] = []
}
hash[keyString].push(obj)
return hash
}, {} as Record<string, T[]>)
}
export function upperFirst(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
export function slugify(string: string) {
return string
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)+/g, '')
}
export function truncate(str: string, maxLength = 20) {
if (str.length <= maxLength) return str
return str.slice(0, maxLength) + '...'
}
export function stripHtml(html: string): string {
const tmp = document.createElement('DIV')
tmp.innerHTML = html
return tmp.textContent || tmp.innerText || ''
}
import type { Content } from '../appContext'
export default {
content: (values: Partial<Content>) => {
const { text, label, slug, url } = values
if ((label ?? '').length < 3) return false
if ((slug ?? '').length < 3) return false
if ((text ?? '').length < 3) return false
if ((url ?? '').length < 3) return false
return true
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment