| |
| |
| |
| |
|
|
| import { type ClassValue, clsx } from "clsx"; |
| import { twMerge } from "tailwind-merge"; |
|
|
| |
|
|
| |
| |
| |
| export function cn(...inputs: ClassValue[]) { |
| return twMerge(clsx(inputs)); |
| } |
|
|
| |
|
|
| |
| |
| |
| export function formatNumber(num: number): string { |
| return new Intl.NumberFormat("en-US").format(num); |
| } |
|
|
| |
| |
| |
| export function formatCurrency(amount: number, currency = "USD"): string { |
| return new Intl.NumberFormat("en-US", { |
| style: "currency", |
| currency, |
| }).format(amount); |
| } |
|
|
| |
| |
| |
| export function formatDate(date: Date | string | null | undefined): string { |
| if (!date) return "N/A"; |
| const d = typeof date === "string" ? new Date(date) : date; |
| return new Intl.DateTimeFormat("en-US", { |
| month: "short", |
| day: "numeric", |
| year: "numeric", |
| }).format(d); |
| } |
|
|
| |
| |
| |
| export function formatDateTime(date: Date | string | null | undefined): string { |
| if (!date) return "N/A"; |
| const d = typeof date === "string" ? new Date(date) : date; |
| return new Intl.DateTimeFormat("en-US", { |
| month: "short", |
| day: "numeric", |
| year: "numeric", |
| hour: "numeric", |
| minute: "2-digit", |
| }).format(d); |
| } |
|
|
| |
| |
| |
| export function formatRelativeTime(date: Date | string | null | undefined): string { |
| if (!date) return "N/A"; |
| const d = typeof date === "string" ? new Date(date) : date; |
| const now = new Date(); |
| const diffInSeconds = Math.floor((now.getTime() - d.getTime()) / 1000); |
|
|
| if (diffInSeconds < 60) return "just now"; |
| if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`; |
| if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`; |
| if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`; |
|
|
| return formatDate(d); |
| } |
|
|
| |
| |
| |
| export function truncate(str: string, maxLength: number): string { |
| if (str.length <= maxLength) return str; |
| return str.slice(0, maxLength - 3) + "..."; |
| } |
|
|
| |
| |
| |
| export function capitalize(str: string): string { |
| return str.charAt(0).toUpperCase() + str.slice(1); |
| } |
|
|
| |
| |
| |
| export function snakeToTitle(str: string): string { |
| return str |
| .split("_") |
| .map(word => capitalize(word)) |
| .join(" "); |
| } |
|
|
| |
|
|
| |
| |
| |
| export function isValidEmail(email: string): boolean { |
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; |
| return emailRegex.test(email); |
| } |
|
|
| |
| |
| |
| export function isValidUrl(url: string): boolean { |
| try { |
| new URL(url); |
| return true; |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| export function sanitizeHtml(html: string): string { |
| const div = document.createElement("div"); |
| div.textContent = html; |
| return div.innerHTML; |
| } |
|
|
| |
|
|
| |
| |
| |
| export function deepClone<T>(obj: T): T { |
| return JSON.parse(JSON.stringify(obj)); |
| } |
|
|
| |
| |
| |
| export function removeEmpty<T extends Record<string, unknown>>(obj: T): Partial<T> { |
| return Object.fromEntries( |
| |
| Object.entries(obj).filter(([_, value]) => value != null) |
| ) as Partial<T>; |
| } |
|
|
| |
| |
| |
| export function groupBy<T>(array: T[], key: keyof T): Record<string, T[]> { |
| return array.reduce((result, item) => { |
| const groupKey = String(item[key]); |
| if (!result[groupKey]) { |
| result[groupKey] = []; |
| } |
| result[groupKey].push(item); |
| return result; |
| }, {} as Record<string, T[]>); |
| } |
|
|
| |
| |
| |
| export function sortBy<T>(array: T[], key: keyof T, order: "asc" | "desc" = "asc"): T[] { |
| return [...array].sort((a, b) => { |
| const aVal = a[key]; |
| const bVal = b[key]; |
|
|
| if (aVal < bVal) return order === "asc" ? -1 : 1; |
| if (aVal > bVal) return order === "asc" ? 1 : -1; |
| return 0; |
| }); |
| } |
|
|
| |
| |
| |
| export function debounce<T extends (...args: unknown[]) => unknown>( |
| func: T, |
| wait: number |
| ): (...args: Parameters<T>) => void { |
| let timeout: NodeJS.Timeout; |
| return function executedFunction(...args: Parameters<T>) { |
| const later = () => { |
| clearTimeout(timeout); |
| func(...args); |
| }; |
| clearTimeout(timeout); |
| timeout = setTimeout(later, wait); |
| }; |
| } |
|
|
| |
| |
| |
| export function throttle<T extends (...args: unknown[]) => unknown>( |
| func: T, |
| limit: number |
| ): (...args: Parameters<T>) => void { |
| let inThrottle: boolean; |
| return function executedFunction(...args: Parameters<T>) { |
| if (!inThrottle) { |
| func(...args); |
| inThrottle = true; |
| setTimeout(() => (inThrottle = false), limit); |
| } |
| }; |
| } |
|
|
| |
|
|
| |
| |
| |
| export function sleep(ms: number): Promise<void> { |
| return new Promise(resolve => setTimeout(resolve, ms)); |
| } |
|
|
| |
| |
| |
| export async function retryWithBackoff<T>( |
| fn: () => Promise<T>, |
| maxRetries = 3, |
| baseDelay = 1000 |
| ): Promise<T> { |
| for (let i = 0; i < maxRetries; i++) { |
| try { |
| return await fn(); |
| } catch (error) { |
| if (i === maxRetries - 1) throw error; |
| await sleep(baseDelay * Math.pow(2, i)); |
| } |
| } |
| throw new Error("Max retries reached"); |
| } |
|
|
| |
|
|
| |
| |
| |
| export function getErrorMessage(error: unknown): string { |
| if (error instanceof Error) return error.message; |
| if (typeof error === "string") return error; |
| return "An unknown error occurred"; |
| } |
|
|
| |
| |
| |
| export function isNetworkError(error: unknown): boolean { |
| return ( |
| error instanceof TypeError && |
| (error.message.includes("fetch") || error.message.includes("network")) |
| ); |
| } |
|
|
| |
|
|
| |
| |
| |
| export function generateId(prefix = ""): string { |
| const random = Math.random().toString(36).substring(2, 15); |
| const timestamp = Date.now().toString(36); |
| return prefix ? `${prefix}_${timestamp}${random}` : `${timestamp}${random}`; |
| } |
|
|
| |
| |
| |
| export function randomColor(): string { |
| return `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0")}`; |
| } |
|
|
| |
|
|
| |
| |
| |
| export async function copyToClipboard(text: string): Promise<boolean> { |
| try { |
| await navigator.clipboard.writeText(text); |
| return true; |
| } catch { |
| |
| const textArea = document.createElement("textarea"); |
| textArea.value = text; |
| textArea.style.position = "fixed"; |
| textArea.style.left = "-999999px"; |
| document.body.appendChild(textArea); |
| textArea.select(); |
| try { |
| document.execCommand("copy"); |
| return true; |
| } catch { |
| return false; |
| } finally { |
| document.body.removeChild(textArea); |
| } |
| } |
| } |
|
|
| |
|
|
| |
| |
| |
| export function formatFileSize(bytes: number): string { |
| if (bytes === 0) return "0 Bytes"; |
| const k = 1024; |
| const sizes = ["Bytes", "KB", "MB", "GB"]; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; |
| } |
|
|
| |
| |
| |
| export function getFileExtension(filename: string): string { |
| return filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2); |
| } |
|
|
| |
|
|
| |
| |
| |
| export function calculatePercentage(value: number, total: number): number { |
| if (total === 0) return 0; |
| return (value / total) * 100; |
| } |
|
|
| |
| |
| |
| export function formatPercentage(value: number, decimals = 1): string { |
| return `${value.toFixed(decimals)}%`; |
| } |
|
|