AutoLoop / lib /utils.ts
shubhjn's picture
feat: Implement core CMS features including workflow management, admin dashboard, API infrastructure, queueing system, and new UI components.
59697b4
/**
* Common Utility Functions
* Reusable helpers for common operations
*/
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
// ==================== STYLING ====================
/**
* Merge Tailwind CSS classes with proper precedence
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// ==================== FORMATTING ====================
/**
* Format number with comma separators
*/
export function formatNumber(num: number): string {
return new Intl.NumberFormat("en-US").format(num);
}
/**
* Format currency
*/
export function formatCurrency(amount: number, currency = "USD"): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(amount);
}
/**
* Format date to readable string
*/
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);
}
/**
* Format date time
*/
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);
}
/**
* Format relative time (e.g., "2 hours ago")
*/
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);
}
/**
* Truncate string to max length
*/
export function truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - 3) + "...";
}
/**
* Capitalize first letter
*/
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Convert snake_case to Title Case
*/
export function snakeToTitle(str: string): string {
return str
.split("_")
.map(word => capitalize(word))
.join(" ");
}
// ==================== VALIDATION ====================
/**
* Validate email format
*/
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* Validate URL format
*/
export function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
/**
* Sanitize HTML to prevent XSS
*/
export function sanitizeHtml(html: string): string {
const div = document.createElement("div");
div.textContent = html;
return div.innerHTML;
}
// ==================== DATA MANIPULATION ====================
/**
* Deep clone an object
*/
export function deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
/**
* Remove undefined/null values from object
*/
export function removeEmpty<T extends Record<string, unknown>>(obj: T): Partial<T> {
return Object.fromEntries(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Object.entries(obj).filter(([_, value]) => value != null)
) as Partial<T>;
}
/**
* Group array by key
*/
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[]>);
}
/**
* Sort array by key
*/
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;
});
}
/**
* Debounce function
*/
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);
};
}
/**
* Throttle function
*/
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);
}
};
}
// ==================== ASYNC HELPERS ====================
/**
* Sleep/delay function
*/
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Retry async function with exponential backoff
*/
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");
}
// ==================== ERROR HANDLING ====================
/**
* Extract error message from unknown error
*/
export function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
return "An unknown error occurred";
}
/**
* Check if error is network error
*/
export function isNetworkError(error: unknown): boolean {
return (
error instanceof TypeError &&
(error.message.includes("fetch") || error.message.includes("network"))
);
}
// ==================== RANDOM ====================
/**
* Generate random ID
*/
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}`;
}
/**
* Generate random color (hex)
*/
export function randomColor(): string {
return `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0")}`;
}
// ==================== COPY TO CLIPBOARD ====================
/**
* Copy text to clipboard
*/
export async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Fallback for older browsers
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);
}
}
}
// ==================== FILE HELPERS ====================
/**
* Format file size to human readable
*/
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]}`;
}
/**
* Get file extension
*/
export function getFileExtension(filename: string): string {
return filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2);
}
// ==================== PERCENTAGE CALCULATION ====================
/**
* Calculate percentage
*/
export function calculatePercentage(value: number, total: number): number {
if (total === 0) return 0;
return (value / total) * 100;
}
/**
* Format percentage
*/
export function formatPercentage(value: number, decimals = 1): string {
return `${value.toFixed(decimals)}%`;
}