diff --git a/frontend/package.json b/frontend/package.json index 83e755c76..75e2a14ac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,7 +52,6 @@ "@sentry/sveltekit": "^10.40.0", "axios": "^1.13.6", "clsx": "^2.1.1", - "date-fns": "^4.1.0", "libphonenumber-js": "^1.12.38", "svelte-dnd-action": "^0.9.69", "tailwind-merge": "^3.5.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 554919181..f20b74e19 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 libphonenumber-js: specifier: ^1.12.38 version: 1.12.38 @@ -1279,9 +1276,6 @@ packages: engines: {node: '>=4'} hasBin: true - date-fns@4.1.0: - resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -3399,8 +3393,6 @@ snapshots: cssesc@3.0.0: {} - date-fns@4.1.0: {} - debug@4.4.3: dependencies: ms: 2.1.3 diff --git a/frontend/src/hooks.server.js b/frontend/src/hooks.server.js index b5c35a319..27fd86f09 100644 --- a/frontend/src/hooks.server.js +++ b/frontend/src/hooks.server.js @@ -231,7 +231,7 @@ export const handle = sequence(Sentry.sentryHandle(), async function _handle({ e } else { // Org switch failed, clear org cookie and redirect event.cookies.delete('org', { path: '/' }); - throw redirect(307, '/org'); + throw redirect(303, '/org'); } } } diff --git a/frontend/src/lib/utils/formatting.js b/frontend/src/lib/utils/formatting.js index 0f05c5fa2..56fb60b62 100644 --- a/frontend/src/lib/utils/formatting.js +++ b/frontend/src/lib/utils/formatting.js @@ -3,18 +3,33 @@ * @module lib/utils/formatting */ -import { format, formatDistanceToNow } from 'date-fns'; +const dateFormatter = new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' +}); + +const relativeFormatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); + +const RELATIVE_UNITS = /** @type {const} */ ([ + { unit: 'year', ms: 365.25 * 24 * 60 * 60 * 1000 }, + { unit: 'month', ms: 30.44 * 24 * 60 * 60 * 1000 }, + { unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 }, + { unit: 'day', ms: 24 * 60 * 60 * 1000 }, + { unit: 'hour', ms: 60 * 60 * 1000 }, + { unit: 'minute', ms: 60 * 1000 }, + { unit: 'second', ms: 1000 } +]); /** - * Format a date string to a human-readable format + * Format a date string to a human-readable format (e.g., "Feb 18, 2026") * @param {string | Date | null | undefined} date - Date to format - * @param {string} [formatStr='MMM d, yyyy'] - date-fns format string * @returns {string} Formatted date string or '-' if no date */ -export function formatDate(date, formatStr = 'MMM d, yyyy') { +export function formatDate(date) { if (!date) return '-'; try { - return format(new Date(date), formatStr); + return dateFormatter.format(new Date(date)); } catch { return '-'; } @@ -28,7 +43,13 @@ export function formatDate(date, formatStr = 'MMM d, yyyy') { export function formatRelativeDate(date) { if (!date) return '-'; try { - return formatDistanceToNow(new Date(date), { addSuffix: true }); + const diff = new Date(date).getTime() - Date.now(); + for (const { unit, ms } of RELATIVE_UNITS) { + if (Math.abs(diff) >= ms || unit === 'second') { + return relativeFormatter.format(Math.round(diff / ms), unit); + } + } + return '-'; } catch { return '-'; } diff --git a/frontend/src/lib/utils/phone.js b/frontend/src/lib/utils/phone.js index b5ae99d44..009f2dec0 100644 --- a/frontend/src/lib/utils/phone.js +++ b/frontend/src/lib/utils/phone.js @@ -1,12 +1,18 @@ -import { isValidPhoneNumber, parsePhoneNumber, isPossiblePhoneNumber } from 'libphonenumber-js'; +/** @type {Promise | null} */ +let _lib = null; + +function getLib() { + if (!_lib) _lib = import('libphonenumber-js'); + return _lib; +} /** * Validates a phone number and returns validation result * @param {string} phoneNumber - The phone number to validate * @param {string} [defaultCountry] - Default country code (e.g., 'US', 'IN') - * @returns {{ isValid: boolean, formatted?: string, error?: string }} + * @returns {Promise<{ isValid: boolean, formatted?: string, error?: string }>} */ -export function validatePhoneNumber(phoneNumber, defaultCountry) { +export async function validatePhoneNumber(phoneNumber, defaultCountry) { if (!phoneNumber || phoneNumber.trim() === '') { return { isValid: true }; // Allow empty phone numbers } @@ -14,6 +20,8 @@ export function validatePhoneNumber(phoneNumber, defaultCountry) { const trimmed = phoneNumber.trim(); try { + const { isValidPhoneNumber, parsePhoneNumber, isPossiblePhoneNumber } = await getLib(); + // If the number starts with +, validate as international if (trimmed.startsWith('+')) { const isValid = isValidPhoneNumber(trimmed); @@ -88,12 +96,13 @@ export function validatePhoneNumber(phoneNumber, defaultCountry) { * Formats a phone number for display * @param {string} phoneNumber - The phone number to format * @param {string} defaultCountry - Default country code - * @returns {string} Formatted phone number or original if invalid + * @returns {Promise} Formatted phone number or original if invalid */ -export function formatPhoneNumber(phoneNumber, defaultCountry = 'US') { +export async function formatPhoneNumber(phoneNumber, defaultCountry = 'US') { if (!phoneNumber) return ''; try { + const { parsePhoneNumber } = await getLib(); // @ts-ignore - defaultCountry is a valid CountryCode const parsed = parsePhoneNumber(phoneNumber, { defaultCountry }); return parsed.formatInternational(); @@ -106,14 +115,16 @@ export function formatPhoneNumber(phoneNumber, defaultCountry = 'US') { * Formats a phone number for storage (E.164 format) * @param {string} phoneNumber - The phone number to format * @param {string} [defaultCountry] - Default country code - * @returns {string} E.164 formatted phone number or original if invalid + * @returns {Promise} E.164 formatted phone number or original if invalid */ -export function formatPhoneForStorage(phoneNumber, defaultCountry) { +export async function formatPhoneForStorage(phoneNumber, defaultCountry) { if (!phoneNumber) return ''; const trimmed = phoneNumber.trim(); try { + const { parsePhoneNumber } = await getLib(); + // If starts with +, parse as international if (trimmed.startsWith('+')) { const parsed = parsePhoneNumber(trimmed); diff --git a/frontend/src/routes/(app)/profile/+page.server.js b/frontend/src/routes/(app)/profile/+page.server.js index 775a2b844..c98644fba 100644 --- a/frontend/src/routes/(app)/profile/+page.server.js +++ b/frontend/src/routes/(app)/profile/+page.server.js @@ -78,11 +78,11 @@ export const actions = { // Validate phone if provided let formattedPhone = null; if (phone && phone.trim().length > 0) { - const phoneValidation = validatePhoneNumber(phone.trim()); + const phoneValidation = await validatePhoneNumber(phone.trim()); if (!phoneValidation.isValid) { errors.phone = phoneValidation.error || 'Please enter a valid phone number'; } else { - formattedPhone = formatPhoneForStorage(phone.trim()); + formattedPhone = await formatPhoneForStorage(phone.trim()); } } diff --git a/frontend/src/routes/(app)/profile/+page.svelte b/frontend/src/routes/(app)/profile/+page.svelte index 9f38fe89e..9c151a97f 100644 --- a/frontend/src/routes/(app)/profile/+page.svelte +++ b/frontend/src/routes/(app)/profile/+page.svelte @@ -18,6 +18,7 @@ let isEditing = $state(false); let isSubmitting = $state(false); let phoneError = $state(''); + let formattedDisplayPhone = $state(''); // Form data state - initialized by $effect below let formData = $state({ @@ -36,14 +37,23 @@ } }); + // Format phone for display (async, resolved into state) + $effect(() => { + if (data.user.phone) { + formatPhoneNumber(data.user.phone).then((f) => (formattedDisplayPhone = f)); + } else { + formattedDisplayPhone = ''; + } + }); + // Validate phone number on input - function validatePhone() { + async function validatePhone() { if (!formData.phone.trim()) { phoneError = ''; return; } - const validation = validatePhoneNumber(formData.phone); + const validation = await validatePhoneNumber(formData.phone); if (!validation.isValid) { phoneError = validation.error || 'Invalid phone number'; } else { @@ -277,7 +287,7 @@ Phone Number

- {data.user.phone ? formatPhoneNumber(data.user.phone) : 'Not provided'} + {formattedDisplayPhone || data.user.phone || 'Not provided'}