107 lines
2.5 KiB
TypeScript
107 lines
2.5 KiB
TypeScript
|
|
/**
|
||
|
|
* Utility functions for the application
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { type ClassValue, clsx } from 'clsx';
|
||
|
|
import { twMerge } from 'tailwind-merge';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Merge Tailwind CSS classes with clsx
|
||
|
|
*/
|
||
|
|
export function cn(...inputs: ClassValue[]) {
|
||
|
|
return twMerge(clsx(inputs));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format a number for display with proper precision
|
||
|
|
*/
|
||
|
|
export function formatNumber(
|
||
|
|
value: number,
|
||
|
|
options: {
|
||
|
|
maxDecimals?: number;
|
||
|
|
minDecimals?: number;
|
||
|
|
notation?: 'standard' | 'scientific' | 'engineering' | 'compact';
|
||
|
|
} = {}
|
||
|
|
): string {
|
||
|
|
const {
|
||
|
|
maxDecimals = 6,
|
||
|
|
minDecimals = 0,
|
||
|
|
notation = 'standard',
|
||
|
|
} = options;
|
||
|
|
|
||
|
|
// Handle edge cases
|
||
|
|
if (!isFinite(value)) return value.toString();
|
||
|
|
if (value === 0) return '0';
|
||
|
|
|
||
|
|
// Use scientific notation for very large or very small numbers
|
||
|
|
const absValue = Math.abs(value);
|
||
|
|
const useScientific =
|
||
|
|
notation === 'scientific' ||
|
||
|
|
(notation === 'standard' && (absValue >= 1e10 || absValue < 1e-6));
|
||
|
|
|
||
|
|
if (useScientific) {
|
||
|
|
return value.toExponential(maxDecimals);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Format with appropriate decimal places
|
||
|
|
const formatted = new Intl.NumberFormat('en-US', {
|
||
|
|
minimumFractionDigits: minDecimals,
|
||
|
|
maximumFractionDigits: maxDecimals,
|
||
|
|
notation: notation === 'compact' ? 'compact' : 'standard',
|
||
|
|
}).format(value);
|
||
|
|
|
||
|
|
return formatted;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Debounce function for input handling
|
||
|
|
*/
|
||
|
|
export function debounce<T extends (...args: any[]) => any>(
|
||
|
|
func: T,
|
||
|
|
wait: number
|
||
|
|
): (...args: Parameters<T>) => void {
|
||
|
|
let timeout: NodeJS.Timeout | null = null;
|
||
|
|
|
||
|
|
return function executedFunction(...args: Parameters<T>) {
|
||
|
|
const later = () => {
|
||
|
|
timeout = null;
|
||
|
|
func(...args);
|
||
|
|
};
|
||
|
|
|
||
|
|
if (timeout) clearTimeout(timeout);
|
||
|
|
timeout = setTimeout(later, wait);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Parse a number input string
|
||
|
|
*/
|
||
|
|
export function parseNumberInput(input: string): number | null {
|
||
|
|
if (!input || input.trim() === '') return null;
|
||
|
|
|
||
|
|
// Remove spaces and replace comma with dot
|
||
|
|
const cleaned = input.replace(/\s/g, '').replace(',', '.');
|
||
|
|
|
||
|
|
const parsed = parseFloat(cleaned);
|
||
|
|
|
||
|
|
return isNaN(parsed) ? null : parsed;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get relative time from timestamp
|
||
|
|
*/
|
||
|
|
export function getRelativeTime(timestamp: number): string {
|
||
|
|
const now = Date.now();
|
||
|
|
const diff = now - timestamp;
|
||
|
|
|
||
|
|
const seconds = Math.floor(diff / 1000);
|
||
|
|
const minutes = Math.floor(seconds / 60);
|
||
|
|
const hours = Math.floor(minutes / 60);
|
||
|
|
const days = Math.floor(hours / 24);
|
||
|
|
|
||
|
|
if (days > 0) return `${days}d ago`;
|
||
|
|
if (hours > 0) return `${hours}h ago`;
|
||
|
|
if (minutes > 0) return `${minutes}m ago`;
|
||
|
|
return 'just now';
|
||
|
|
}
|