mirror of
https://github.com/vas3k/TaxHacker.git
synced 2026-01-08 14:40:26 -06:00
* feat: visual color picker * feat: use pallete for random colors * fix: style issues like indentation in code and different color format
159 lines
4.4 KiB
TypeScript
159 lines
4.4 KiB
TypeScript
import { clsx, type ClassValue } from "clsx"
|
|
import slugify from "slugify"
|
|
import { twMerge } from "tailwind-merge"
|
|
import { violet, tomato, red, crimson, pink, plum, purple, indigo, blue, sky, cyan, teal, mint, grass, lime, yellow, amber, orange, brown } from "@radix-ui/colors"
|
|
|
|
const LOCALE = "en-US"
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs))
|
|
}
|
|
|
|
export function formatCurrency(total: number, currency: string) {
|
|
try {
|
|
return new Intl.NumberFormat(LOCALE, {
|
|
style: "currency",
|
|
currency: currency,
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
useGrouping: true,
|
|
}).format(total / 100)
|
|
} catch (error) {
|
|
// can happen with custom currencies and crypto
|
|
return `${currency} ${total / 100}`
|
|
}
|
|
}
|
|
|
|
export function formatBytes(bytes: number) {
|
|
if (bytes === 0) return "0 Bytes"
|
|
|
|
const sizes = ["Bytes", "KB", "MB", "GB"]
|
|
const maxIndex = sizes.length - 1
|
|
|
|
const i = Math.min(Math.floor(Math.log10(bytes) / Math.log10(1024)), maxIndex)
|
|
const value = bytes / Math.pow(1024, i)
|
|
|
|
return `${parseFloat(value.toFixed(2))} ${sizes[i]}`
|
|
}
|
|
|
|
export function formatNumber(number: number) {
|
|
return new Intl.NumberFormat(LOCALE, {
|
|
useGrouping: true,
|
|
}).format(number)
|
|
}
|
|
|
|
export function codeFromName(name: string, maxLength: number = 16) {
|
|
const code = slugify(name, {
|
|
replacement: "_",
|
|
lower: true,
|
|
strict: true,
|
|
trim: true,
|
|
})
|
|
return code.slice(0, maxLength)
|
|
}
|
|
|
|
export function randomHexColor() {
|
|
const palette = [
|
|
violet.violet9,
|
|
indigo.indigo9,
|
|
blue.blue9,
|
|
cyan.cyan9,
|
|
teal.teal9,
|
|
grass.grass9,
|
|
lime.lime9,
|
|
yellow.yellow9,
|
|
amber.amber9,
|
|
orange.orange9,
|
|
red.red9,
|
|
crimson.crimson9,
|
|
pink.pink9,
|
|
purple.purple9,
|
|
plum.plum9,
|
|
mint.mint9,
|
|
brown.brown9,
|
|
sky.sky9,
|
|
tomato.tomato9,
|
|
]
|
|
return palette[Math.floor(Math.random() * palette.length)]
|
|
}
|
|
|
|
export async function fetchAsBase64(url: string): Promise<string | null> {
|
|
try {
|
|
const response = await fetch(url)
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch image: ${response.statusText}`)
|
|
}
|
|
|
|
const blob = await response.blob()
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
const reader = new FileReader()
|
|
reader.onloadend = () => resolve(reader.result as string)
|
|
reader.onerror = reject
|
|
reader.readAsDataURL(blob)
|
|
})
|
|
} catch (error) {
|
|
console.error("Error fetching image as data URL:", error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function encodeFilename(filename: string): string {
|
|
const encoded = encodeURIComponent(filename)
|
|
return `UTF-8''${encoded}`
|
|
}
|
|
|
|
export function generateUUID(): string {
|
|
// Try to use crypto.randomUUID() if available (modern browsers and Node.js 14.17+)
|
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
try {
|
|
return crypto.randomUUID()
|
|
} catch (error) {
|
|
// Fall through to next method
|
|
}
|
|
}
|
|
|
|
// Fallback to crypto.getRandomValues() for UUID v4 generation
|
|
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
try {
|
|
const bytes = new Uint8Array(16)
|
|
crypto.getRandomValues(bytes)
|
|
|
|
// Set version (4) and variant bits according to RFC 4122
|
|
bytes[6] = (bytes[6] & 0x0f) | 0x40 // Version 4
|
|
bytes[8] = (bytes[8] & 0x3f) | 0x80 // Variant 10
|
|
|
|
// Convert to UUID string format
|
|
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")
|
|
return [hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20, 32)].join("-")
|
|
} catch (error) {
|
|
// Fall through to Math.random() fallback
|
|
}
|
|
}
|
|
|
|
// Final fallback using Math.random() (RFC 4122 compliant UUID v4)
|
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
|
const r = (Math.random() * 16) | 0
|
|
const v = c === "x" ? r : (r & 0x3) | 0x8
|
|
return v.toString(16)
|
|
})
|
|
}
|
|
|
|
export function formatPeriodLabel(period: string, date: Date): string {
|
|
if (period.includes("-") && period.split("-").length === 3) {
|
|
// Daily format: show day/month/year
|
|
return date.toLocaleDateString("en-US", {
|
|
weekday: "short",
|
|
month: "short",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
})
|
|
} else {
|
|
// Monthly format: show month/year with short month name
|
|
return date.toLocaleDateString("en-US", {
|
|
month: "short",
|
|
year: "numeric",
|
|
})
|
|
}
|
|
}
|