// ─── Locale ─────────────────────────────────────────────────────────────────── export const APP_LOCALE = "en-GB"; export const APP_CURRENCY = "GBP"; // ─── Currency ───────────────────────────────────────────────────────────────── /** * Format cents to a currency string e.g. 1999 -> "£19.99" */ export function formatPrice( cents: number, currency = APP_CURRENCY, locale = APP_LOCALE ): string { return new Intl.NumberFormat(locale, { style: "currency", currency, minimumFractionDigits: 2, }).format(cents / 100); } /** * Convert dollars to cents for storing in DB e.g. 19.99 -> 1999 */ export function dollarsToCents(dollars: number): number { return Math.round(dollars * 100); } /** * Convert cents to dollars e.g. 1999 -> 19.99 */ export function centsToDollars(cents: number): number { return cents / 100; } // ─── Slugs ─────────────────────────────────────────────────────────────────── /** * Generate a URL-safe slug from a string * e.g. "Hello World!" -> "hello-world" */ export function slugify(text: string): string { return text .toLowerCase() .trim() .replace(/[^\w\s-]/g, "") .replace(/[\s_-]+/g, "-") .replace(/^-+|-+$/g, ""); } // ─── Dates ─────────────────────────────────────────────────────────────────── /** * Format an ISO date string to a readable format * e.g. "2024-01-15T10:30:00Z" -> "Jan 15, 2024" */ export function formatDate( isoString: string, options: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric", } ): string { return new Intl.DateTimeFormat(APP_LOCALE, options).format( new Date(isoString) ); } /** * Format an ISO date string to a relative time * e.g. "2 hours ago", "3 days ago" */ export function formatRelativeTime(isoString: string): string { const diff = Date.now() - new Date(isoString).getTime(); 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} day${days > 1 ? "s" : ""} ago`; if (hours > 0) return `${hours} hour${hours > 1 ? "s" : ""} ago`; if (minutes > 0) return `${minutes} minute${minutes > 1 ? "s" : ""} ago`; return "just now"; } // ─── Cart Helpers ─────────────────────────────────────────────────────────── export interface CartTotalsInput { quantity: number; priceSnapshot?: number; unitPrice?: number; } /** * Calculate subtotal, total, and item count from cart items. * Uses priceSnapshot or unitPrice (cents). Used in cart UI and before order creation. */ export function calculateCartTotals( items: CartTotalsInput[] ): { subtotal: number; total: number; itemCount: number } { let subtotal = 0; let itemCount = 0; for (const item of items) { const price = item.priceSnapshot ?? item.unitPrice ?? 0; subtotal += item.quantity * price; itemCount += item.quantity; } return { subtotal, total: subtotal, itemCount }; } // ─── Order Helpers ─────────────────────────────────────────────────────────── import type { OrderStatus, PaymentStatus } from "@repo/types"; export const ORDER_STATUS_LABELS: Record = { pending: "Pending", confirmed: "Confirmed", processing: "Processing", shipped: "Shipped", delivered: "Delivered", cancelled: "Cancelled", refunded: "Refunded", return: "Return Requested", completed: "Completed", }; export const PAYMENT_STATUS_LABELS: Record = { pending: "Pending", paid: "Paid", failed: "Failed", refunded: "Refunded", }; export const ORDER_STATUS_COLORS: Record = { pending: "yellow", confirmed: "blue", processing: "purple", shipped: "indigo", delivered: "green", cancelled: "red", refunded: "gray", return: "orange", completed: "teal", }; // ─── Validation ─────────────────────────────────────────────────────────────── export function isValidEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } export function isValidPhone(phone: string): boolean { return /^\+?[\d\s\-()]{7,15}$/.test(phone); } // ─── Pagination ─────────────────────────────────────────────────────────────── export function getPaginationRange( page: number, limit: number ): { from: number; to: number } { const from = (page - 1) * limit; const to = from + limit - 1; return { from, to }; } export function getTotalPages(total: number, limit: number): number { return Math.ceil(total / limit); } // ─── SKU Generation ─────────────────────────────────────────────────────────── /** * Generate a SKU from product metadata. * e.g. Royal Canin, Adult Dog Food, flavor: Chicken, 5kg → "ROY-CANI-ADUL-DOG-CHIC-5KG" */ export function generateSku( brand: string, productName: string, attributes?: { size?: string; flavor?: string; color?: string; }, weight?: number, weightUnit?: string ): string { const clean = (str: string) => str .toUpperCase() .trim() .replace(/[^A-Z0-9\s]/g, "") .split(/\s+/) .map((w) => w.slice(0, 4)) .join("-"); const parts = [ brand ? clean(brand) : null, productName ? clean(productName) : null, attributes?.flavor ? clean(attributes.flavor) : null, attributes?.size ? clean(attributes.size) : null, attributes?.color ? clean(attributes.color) : null, weight && weightUnit ? `${weight}${weightUnit.toUpperCase()}` : null, ].filter(Boolean); return parts.join("-"); } // ─── Misc ───────────────────────────────────────────────────────────────────── /** * Generate a short order number e.g. "ORD-4F2K9" */ export function generateOrderNumber(): string { return `ORD-${Math.random().toString(36).substring(2, 7).toUpperCase()}`; } /** * Truncate a string to a max length with ellipsis */ export function truncate(text: string, maxLength: number): string { if (text.length <= maxLength) return text; return `${text.substring(0, maxLength)}...`; } /** * Deep clone an object safely */ export function deepClone(obj: T): T { return JSON.parse(JSON.stringify(obj)); }