hype/src/utils.tsx

172 lines
4.8 KiB
TypeScript

import { type Context } from 'hono'
import { stat } from 'fs/promises'
// template literal tag for inline CSS. returns a <style> tag
export function css(strings: TemplateStringsArray, ...values: any[]) {
return <style dangerouslySetInnerHTML={{
__html: String.raw({ raw: strings }, ...values)
}} />
}
const transpiler = new Bun.Transpiler({ loader: 'tsx' })
// template literal tag for inline JS. transpiles and returns a <script> tag
export function js(strings: TemplateStringsArray, ...values: any[]) {
return <script dangerouslySetInnerHTML={{
__html: transpiler.transformSync(String.raw({ raw: strings }, ...values))
}} />
}
// lighten a hex color by blending with white. opacity 1 = original, 0 = white
export function lightenColor(hex: string, opacity: number): string {
// Remove # if present
hex = hex.replace('#', '')
// Convert to RGB
let r = parseInt(hex.substring(0, 2), 16)
let g = parseInt(hex.substring(2, 4), 16)
let b = parseInt(hex.substring(4, 6), 16)
// Blend with white
r = Math.round(r * opacity + 255 * (1 - opacity))
g = Math.round(g * opacity + 255 * (1 - opacity))
b = Math.round(b * opacity + 255 * (1 - opacity))
// Convert back to hex
return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')
}
// darken a hex color by blending with black. opacity 1 = original, 0 = black
export function darkenColor(hex: string, opacity: number): string {
hex = hex.replace('#', '')
let r = parseInt(hex.substring(0, 2), 16)
let g = parseInt(hex.substring(2, 4), 16)
let b = parseInt(hex.substring(4, 6), 16)
// Blend with black (0, 0, 0)
r = Math.round(r * opacity)
g = Math.round(g * opacity)
b = Math.round(b * opacity)
return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')
}
// check if the user prefers dark mode
export function isDarkMode(): boolean {
return window.matchMedia('(prefers-color-scheme: dark)').matches
}
// capitalize a word. that's it.
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
// Generate a 6 character random ID
export function randomId(): string {
return Math.random().toString(36).slice(7)
}
// times(3) returns [1, 2, 3]
export function times(n: number): number[] {
let out = [], i = 1
while (i <= n) out.push(i++)
return out
}
// inclusive
// rand(2) == flip a coin
// rand(6) == roll a die
// rand(20) == dnd
export function rand(end = 2, startAtZero = false): number {
const start = startAtZero ? 0 : 1
return Math.floor(Math.random() * (end - start + 1)) + start
}
// randRange(1, 2) == flip a coin
// randRange(1, 6) == roll a die
// randRange(1, 20) == dnd
export function randRange(start = 0, end = 12): number {
return Math.floor(Math.random() * (end - start + 1)) + start
}
// randomItem([5, 7, 9]) #=> 7
export function randItem<T>(list: T[]): T | undefined {
if (list.length === 0) return
return list[randRange(0, list.length - 1)]
}
// randomIndex([5, 7, 9]) #=> 1
export function randIndex<T>(list: T[]): number | undefined {
if (!list.length) return
return randRange(0, list.length - 1)
}
// unique([1,1,2,2,3,3]) #=> [1,2,3]
export function unique<T>(array: T[]): T[] {
return [...new Set(array)]
}
// shuffle a copy of an array
export function shuffle<T>(arr: readonly T[]): T[] {
const out = arr.slice()
for (let i = out.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const tmp = out[i]!
out[i] = out[j]!
out[j] = tmp
}
return out
}
// random number between 1 and 10, with decreasing probability
export function weightedRand(): number {
// Weights: 1 has weight 10, 2 has weight 9, ..., 10 has weight 1
const weights = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0)
let random = Math.random() * totalWeight
for (let i = 0; i < weights.length; i++) {
random -= weights[i]!
if (random <= 0) {
return numbers[i]!
}
}
return numbers[numbers.length - 1]!
}
const transpileCache: Record<string, string> = {}
// transpile frontend ts to js
export async function transpile(path: string): Promise<string> {
const { mtime } = await stat(path)
const key = `${path}?${mtime}`
// no caching in dev mode
let cached = process.env.NODE_ENV === 'production' ? transpileCache[key] : undefined
if (!cached) {
const result = await Bun.build({
entrypoints: [path],
format: 'esm',
minify: false,
sourcemap: 'none',
})
if (!result.outputs[0]) throw new Error(`Failed to build ${path}`)
cached = await result.outputs[0].text()
transpileCache[key] = cached
}
return cached
}
// redirect to the referrer, or fallback if no referrer
export function redirectBack(c: Context, fallback = "/") {
return c.redirect(c.req.header("Referer") || fallback)
}