This commit is contained in:
Iliyan Angelov
2025-11-16 20:05:08 +02:00
parent 98ccd5b6ff
commit 48353cde9c
118 changed files with 9488 additions and 1336 deletions

View File

@@ -0,0 +1,53 @@
/**
* Application constants
*/
export const API_TIMEOUT = 30000; // 30 seconds
export const MAX_RETRIES = 3;
export const RETRY_DELAY = 1000; // 1 second
export const DATE_FORMATS = {
SHORT: 'short',
MEDIUM: 'medium',
LONG: 'long',
FULL: 'full',
} as const;
export const CURRENCY = {
VND: 'VND',
USD: 'USD',
EUR: 'EUR',
} as const;
export const STORAGE_KEYS = {
TOKEN: 'token',
USER_INFO: 'userInfo',
GUEST_FAVORITES: 'guestFavorites',
THEME: 'theme',
LANGUAGE: 'language',
} as const;
export const ROUTES = {
HOME: '/',
LOGIN: '/login',
REGISTER: '/register',
DASHBOARD: '/dashboard',
ROOMS: '/rooms',
BOOKINGS: '/bookings',
FAVORITES: '/favorites',
PROFILE: '/profile',
ADMIN: '/admin',
} as const;
export const TOAST_DURATION = {
SUCCESS: 3000,
ERROR: 5000,
WARNING: 4000,
INFO: 3000,
} as const;
export const PAGINATION = {
DEFAULT_PAGE_SIZE: 10,
PAGE_SIZE_OPTIONS: [10, 20, 50, 100],
} as const;

View File

@@ -0,0 +1,162 @@
/**
* Utility functions for formatting data
*/
/**
* Format currency
*/
export const formatCurrency = (
amount: number | string,
currency: string = 'VND'
): string => {
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
if (isNaN(numAmount)) return '0 ₫';
if (currency === 'VND') {
return new Intl.NumberFormat('vi-VN', {
style: 'currency',
currency: 'VND',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(numAmount);
}
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
}).format(numAmount);
};
/**
* Format date
*/
export const formatDate = (
date: string | Date,
format: 'short' | 'medium' | 'long' | 'full' = 'medium'
): string => {
const dateObj = typeof date === 'string' ? new Date(date) : date;
if (isNaN(dateObj.getTime())) return 'Invalid Date';
const formatOptions: Record<string, Intl.DateTimeFormatOptions> = {
short: { year: 'numeric', month: 'numeric', day: 'numeric' },
medium: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric' },
full: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
},
};
return new Intl.DateTimeFormat('en-US', formatOptions[format]).format(dateObj);
};
/**
* Format date and time
*/
export const formatDateTime = (
date: string | Date,
includeSeconds: boolean = false
): string => {
const dateObj = typeof date === 'string' ? new Date(date) : date;
if (isNaN(dateObj.getTime())) return 'Invalid Date';
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
...(includeSeconds && { second: '2-digit' }),
}).format(dateObj);
};
/**
* Format relative time (e.g., "2 hours ago")
*/
export const formatRelativeTime = (date: string | Date): string => {
const dateObj = typeof date === 'string' ? new Date(date) : date;
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - dateObj.getTime()) / 1000);
if (diffInSeconds < 0) return 'in the future';
if (diffInSeconds < 60) return 'just now';
if (diffInSeconds < 3600) {
const minutes = Math.floor(diffInSeconds / 60);
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
}
if (diffInSeconds < 86400) {
const hours = Math.floor(diffInSeconds / 3600);
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
}
if (diffInSeconds < 604800) {
const days = Math.floor(diffInSeconds / 86400);
return `${days} day${days > 1 ? 's' : ''} ago`;
}
return formatDate(dateObj, 'short');
};
/**
* Format number
*/
export const formatNumber = (
num: number | string,
decimals: number = 0
): string => {
const numValue = typeof num === 'string' ? parseFloat(num) : num;
if (isNaN(numValue)) return '0';
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(numValue);
};
/**
* Format phone number
*/
export const formatPhoneNumber = (phone: string): string => {
const cleaned = phone.replace(/\D/g, '');
if (cleaned.length === 10) {
return cleaned.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
}
if (cleaned.length === 11) {
return cleaned.replace(/(\d{1})(\d{3})(\d{3})(\d{4})/, '+$1 ($2) $3-$4');
}
return phone;
};
/**
* Format file size
*/
export const 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]}`;
};
/**
* Truncate text
*/
export const truncateText = (
text: string,
maxLength: number,
suffix: string = '...'
): string => {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength - suffix.length) + suffix;
};

View File

@@ -0,0 +1,4 @@
export * from './format';
export * from './constants';
export * from './validationSchemas';