utils.ts

1import { clsx, type ClassValue } from 'clsx'
2import { twMerge } from 'tailwind-merge'
3
4interface IPronounsOptions {
5	isNormalized?: boolean
6	returnAsString?: boolean
7}
8
9interface Pronouns {
10	subject: string
11	object: string
12	possessive: string
13	reflexive: string
14}
15
16export function cn(...inputs: ClassValue[]) {
17	return twMerge(clsx(inputs))
18}
19
20export function makePronouns(
21	pronouns: string,
22	options: IPronounsOptions = {},
23): Pronouns | string {
24	const [subject, object, possessive = '', reflexive = ''] = pronouns.split('/')
25
26	const subjectNormalized = options.isNormalized ? capitalize(subject) : subject
27	const objectNormalized = options.isNormalized ? capitalize(object) : object
28	const possessiveNormalized =
29		options.isNormalized ? capitalize(possessive) : possessive
30	const reflexiveNormalized =
31		options.isNormalized ? capitalize(reflexive) : reflexive
32
33	if (options.returnAsString) {
34		return `${subjectNormalized}/${objectNormalized}/${possessiveNormalized}/${reflexiveNormalized}`
35	}
36	return {
37		subject: subjectNormalized,
38		object: objectNormalized,
39		possessive: possessiveNormalized,
40		reflexive: reflexiveNormalized,
41	}
42}
43
44function capitalize(str: string): string {
45	return str.charAt(0).toUpperCase() + str.slice(1)
46}
47
48export function escapeHtml(unsafe: string): string {
49	return unsafe
50		.replace(/&/g, '&')
51		.replace(/</g, '&lt;')
52		.replace(/>/g, '&gt;')
53		.replace(/"/g, '&quot;')
54		.replace(/'/g, '&apos;')
55		.replace(/`/g, '&#x60;')
56		.replace(/\//g, '&#x2F;')
57}
58
59export function unescapeHtml(safe: string): string {
60	return safe
61		.replace(/&amp;/g, '&')
62		.replace(/&lt;/g, '<')
63		.replace(/&gt;/g, '>')
64		.replace(/&quot;/g, '"')
65		.replace(/&apos;/g, "'")
66		.replace(/&#x60;/g, '`')
67		.replace(/&#x2F;/g, '/')
68		.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(Number(dec))) // Decimal entities
69		.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) =>
70			String.fromCharCode(parseInt(hex, 16)),
71		) // Hexadecimal entities
72}
73
74export function formatDate(date: string, includeRelative = false) {
75	const currentDate = new Date()
76	if (!date.includes('T')) {
77		date = `${date}T00:00:00`
78	}
79	const targetDate = new Date(date)
80
81	const yearsAgo = currentDate.getFullYear() - targetDate.getFullYear()
82	const monthsAgo = currentDate.getMonth() - targetDate.getMonth()
83	const daysAgo = currentDate.getDate() - targetDate.getDate()
84
85	let formattedDate = ''
86
87	if (yearsAgo > 0) {
88		formattedDate = `${yearsAgo}y ago`
89	} else if (monthsAgo > 0) {
90		formattedDate = `${monthsAgo}mo ago`
91	} else if (daysAgo > 0) {
92		formattedDate = `${daysAgo}d ago`
93	} else {
94		formattedDate = 'Today'
95	}
96
97	const fullDate = targetDate.toLocaleString('en-us', {
98		month: 'long',
99		day: 'numeric',
100		year: 'numeric',
101	})
102
103	if (!includeRelative) {
104		return fullDate
105	}
106
107	return `${fullDate} (${formattedDate})`
108}
109