ADD new frontend
This commit is contained in:
388
frontend/src/utils/format.ts
Normal file
388
frontend/src/utils/format.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* Formatting utilities for various data types
|
||||
*/
|
||||
|
||||
// Number formatting
|
||||
export const formatNumber = (
|
||||
value: number,
|
||||
options: Intl.NumberFormatOptions = {}
|
||||
): string => {
|
||||
if (typeof value !== 'number' || isNaN(value)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const defaultOptions: Intl.NumberFormatOptions = {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
return new Intl.NumberFormat('es-ES', { ...defaultOptions, ...options }).format(value);
|
||||
};
|
||||
|
||||
// Currency formatting
|
||||
export const formatCurrency = (
|
||||
amount: number,
|
||||
currency: string = 'EUR',
|
||||
locale: string = 'es-ES'
|
||||
): string => {
|
||||
if (typeof amount !== 'number' || isNaN(amount)) {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
}).format(0);
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// Percentage formatting
|
||||
export const formatPercentage = (
|
||||
value: number,
|
||||
decimals: number = 1,
|
||||
locale: string = 'es-ES'
|
||||
): string => {
|
||||
if (typeof value !== 'number' || isNaN(value)) {
|
||||
return '0%';
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(value / 100);
|
||||
};
|
||||
|
||||
// Compact number formatting (1K, 1M, etc.)
|
||||
export const formatCompactNumber = (
|
||||
value: number,
|
||||
locale: string = 'es-ES'
|
||||
): string => {
|
||||
if (typeof value !== 'number' || isNaN(value)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale, {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
// Decimal formatting with specific precision
|
||||
export const formatDecimal = (
|
||||
value: number,
|
||||
decimals: number = 2,
|
||||
locale: string = 'es-ES'
|
||||
): string => {
|
||||
if (typeof value !== 'number' || isNaN(value)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale, {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
// File size formatting
|
||||
export const formatFileSize = (bytes: number): string => {
|
||||
if (typeof bytes !== 'number' || isNaN(bytes)) {
|
||||
return '0 B';
|
||||
}
|
||||
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const size = bytes / Math.pow(1024, i);
|
||||
|
||||
return `${formatDecimal(size, i === 0 ? 0 : 1)} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
// Duration formatting (milliseconds to human readable)
|
||||
export const formatDuration = (milliseconds: number): string => {
|
||||
if (typeof milliseconds !== 'number' || isNaN(milliseconds) || milliseconds < 0) {
|
||||
return '0s';
|
||||
}
|
||||
|
||||
const seconds = Math.floor(milliseconds / 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 ${hours % 24}h`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
};
|
||||
|
||||
// Short duration formatting (for performance metrics)
|
||||
export const formatShortDuration = (milliseconds: number): string => {
|
||||
if (typeof milliseconds !== 'number' || isNaN(milliseconds)) {
|
||||
return '0ms';
|
||||
}
|
||||
|
||||
if (milliseconds < 1000) {
|
||||
return `${Math.round(milliseconds)}ms`;
|
||||
}
|
||||
|
||||
const seconds = milliseconds / 1000;
|
||||
if (seconds < 60) {
|
||||
return `${formatDecimal(seconds, 1)}s`;
|
||||
}
|
||||
|
||||
const minutes = seconds / 60;
|
||||
return `${formatDecimal(minutes, 1)}m`;
|
||||
};
|
||||
|
||||
// Text formatting
|
||||
export const truncateText = (
|
||||
text: string,
|
||||
maxLength: number,
|
||||
suffix: string = '...'
|
||||
): string => {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (text.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.slice(0, maxLength - suffix.length) + suffix;
|
||||
};
|
||||
|
||||
// Capitalize first letter
|
||||
export const capitalize = (text: string): string => {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||
};
|
||||
|
||||
// Title case formatting
|
||||
export const titleCase = (text: string): string => {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return text
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// Kebab case to title case
|
||||
export const kebabToTitle = (text: string): string => {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return text
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// Snake case to title case
|
||||
export const snakeToTitle = (text: string): string => {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return text
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// Camel case to title case
|
||||
export const camelToTitle = (text: string): string => {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return text
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase())
|
||||
.trim();
|
||||
};
|
||||
|
||||
// Phone number formatting
|
||||
export const formatPhoneNumber = (phone: string): string => {
|
||||
if (!phone || typeof phone !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove all non-digit characters
|
||||
const digits = phone.replace(/\D/g, '');
|
||||
|
||||
// Spanish phone number format
|
||||
if (digits.length === 9) {
|
||||
return `${digits.slice(0, 3)} ${digits.slice(3, 6)} ${digits.slice(6)}`;
|
||||
}
|
||||
|
||||
// International format
|
||||
if (digits.length === 11 && digits.startsWith('34')) {
|
||||
return `+34 ${digits.slice(2, 5)} ${digits.slice(5, 8)} ${digits.slice(8)}`;
|
||||
}
|
||||
|
||||
// Return original if doesn't match expected formats
|
||||
return phone;
|
||||
};
|
||||
|
||||
// Unit of measure formatting
|
||||
export const formatUnit = (value: number, unit: string): string => {
|
||||
if (typeof value !== 'number' || isNaN(value)) {
|
||||
return `0 ${unit}`;
|
||||
}
|
||||
|
||||
const formattedValue = value % 1 === 0 ? value.toString() : formatDecimal(value);
|
||||
return `${formattedValue} ${unit}`;
|
||||
};
|
||||
|
||||
// Weight formatting with unit conversion
|
||||
export const formatWeight = (grams: number): string => {
|
||||
if (typeof grams !== 'number' || isNaN(grams)) {
|
||||
return '0 g';
|
||||
}
|
||||
|
||||
if (grams >= 1000) {
|
||||
const kg = grams / 1000;
|
||||
return `${formatDecimal(kg)} kg`;
|
||||
}
|
||||
|
||||
return `${formatDecimal(grams, 0)} g`;
|
||||
};
|
||||
|
||||
// Volume formatting with unit conversion
|
||||
export const formatVolume = (milliliters: number): string => {
|
||||
if (typeof milliliters !== 'number' || isNaN(milliliters)) {
|
||||
return '0 ml';
|
||||
}
|
||||
|
||||
if (milliliters >= 1000) {
|
||||
const liters = milliliters / 1000;
|
||||
return `${formatDecimal(liters)} l`;
|
||||
}
|
||||
|
||||
return `${formatDecimal(milliliters, 0)} ml`;
|
||||
};
|
||||
|
||||
// Temperature formatting
|
||||
export const formatTemperature = (
|
||||
celsius: number,
|
||||
unit: 'C' | 'F' = 'C'
|
||||
): string => {
|
||||
if (typeof celsius !== 'number' || isNaN(celsius)) {
|
||||
return `0°${unit}`;
|
||||
}
|
||||
|
||||
if (unit === 'F') {
|
||||
const fahrenheit = (celsius * 9/5) + 32;
|
||||
return `${formatDecimal(fahrenheit, 0)}°F`;
|
||||
}
|
||||
|
||||
return `${formatDecimal(celsius, 0)}°C`;
|
||||
};
|
||||
|
||||
// List formatting (join with commas and "and")
|
||||
export const formatList = (
|
||||
items: string[],
|
||||
conjunction: string = 'y'
|
||||
): string => {
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (items.length === 1) {
|
||||
return items[0];
|
||||
}
|
||||
|
||||
if (items.length === 2) {
|
||||
return `${items[0]} ${conjunction} ${items[1]}`;
|
||||
}
|
||||
|
||||
const lastItem = items[items.length - 1];
|
||||
const otherItems = items.slice(0, -1);
|
||||
return `${otherItems.join(', ')} ${conjunction} ${lastItem}`;
|
||||
};
|
||||
|
||||
// Address formatting
|
||||
export const formatAddress = (address: {
|
||||
street?: string;
|
||||
city?: string;
|
||||
postal_code?: string;
|
||||
country?: string;
|
||||
}): string => {
|
||||
const parts = [];
|
||||
|
||||
if (address.street) parts.push(address.street);
|
||||
if (address.postal_code && address.city) {
|
||||
parts.push(`${address.postal_code} ${address.city}`);
|
||||
} else if (address.city) {
|
||||
parts.push(address.city);
|
||||
} else if (address.postal_code) {
|
||||
parts.push(address.postal_code);
|
||||
}
|
||||
if (address.country) parts.push(address.country);
|
||||
|
||||
return parts.join(', ');
|
||||
};
|
||||
|
||||
// HTML stripping (for displaying rich text as plain text)
|
||||
export const stripHtml = (html: string): string => {
|
||||
if (!html || typeof html !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Create a temporary div element to strip HTML tags
|
||||
const tmp = document.createElement('div');
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || '';
|
||||
};
|
||||
|
||||
// URL slug generation
|
||||
export const slugify = (text: string): string => {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w ]+/g, '')
|
||||
.replace(/ +/g, '-');
|
||||
};
|
||||
|
||||
// Name formatting (last name, first name)
|
||||
export const formatName = (
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
format: 'first_last' | 'last_first' | 'initials' = 'first_last'
|
||||
): string => {
|
||||
const first = firstName?.trim() || '';
|
||||
const last = lastName?.trim() || '';
|
||||
|
||||
if (!first && !last) return '';
|
||||
if (!first) return last;
|
||||
if (!last) return first;
|
||||
|
||||
switch (format) {
|
||||
case 'last_first':
|
||||
return `${last}, ${first}`;
|
||||
case 'initials':
|
||||
return `${first.charAt(0)}.${last.charAt(0)}.`;
|
||||
case 'first_last':
|
||||
default:
|
||||
return `${first} ${last}`;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user