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, '<')
52 .replace(/>/g, '>')
53 .replace(/"/g, '"')
54 .replace(/'/g, ''')
55 .replace(/`/g, '`')
56 .replace(/\//g, '/')
57}
58
59export function unescapeHtml(safe: string): string {
60 return safe
61 .replace(/&/g, '&')
62 .replace(/</g, '<')
63 .replace(/>/g, '>')
64 .replace(/"/g, '"')
65 .replace(/'/g, "'")
66 .replace(/`/g, '`')
67 .replace(///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