172 lines
4.8 KiB
TypeScript
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)
|
|
} |