Format date displayed in Releases section (#14183)

Closes #14177 

I noticed a `formatDisplayDate` file being used in `twenty-website` and
created the same one for `twenty-front` as well. Unit tests for the same
have been added.

<img width="1438" height="770" alt="image"
src="https://github.com/user-attachments/assets/5aa6eef5-19c5-4108-bf3f-c582d0ca1b59"
/>
<img width="1275" height="785" alt="image"
src="https://github.com/user-attachments/assets/20a87ba6-fca4-472c-a691-362901505303"
/>

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Balaji Krishnamurthy
2025-09-04 20:57:31 +05:30
committed by GitHub
parent 40251d34ec
commit 7a999c8476
23 changed files with 344 additions and 264 deletions
+66 -107
View File
@@ -1,16 +1,24 @@
/* eslint-disable @nx/workspace-explicit-boolean-predicates-in-if */
import { isDate, isNumber, isString } from '@sniptt/guards';
import { differenceInCalendarDays, formatDistanceToNow } from 'date-fns';
import {
differenceInCalendarDays,
formatDistance,
formatDistanceToNow,
type Locale,
} from 'date-fns';
import { DateTime } from 'luxon';
import moize from 'moize';
import { DateFormat } from '@/localization/constants/DateFormat';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils';
import { CustomError } from '@/error-handler/CustomError';
import { i18n } from '@lingui/core';
import { plural, t } from '@lingui/core/macro';
import { logError } from './logError';
export const DEFAULT_DATE_LOCALE = 'en-EN';
const getLuxonLocale = () => {
return SOURCE_LOCALE === 'en' ? 'en-US' : SOURCE_LOCALE;
};
export const parseDate = (dateToParse: Date | string | number) => {
if (dateToParse === 'now') return DateTime.fromJSDate(new Date());
@@ -44,7 +52,7 @@ export const parseDate = (dateToParse: Date | string | number) => {
);
}
return formattedDate.setLocale(DEFAULT_DATE_LOCALE);
return formattedDate.setLocale(getLuxonLocale());
};
const isSameDay = (a: DateTime, b: DateTime): boolean =>
@@ -73,40 +81,33 @@ export const beautifyExactDateTime = (
export const beautifyExactDate = (dateToBeautify: Date | string | number) => {
const isToday = isSameDay(parseDate(dateToBeautify), DateTime.local());
const dateFormat = isToday ? "'Today'" : 'DD';
return formatDate(dateToBeautify, dateFormat);
if (isToday) {
return t`Today`;
}
return formatDate(dateToBeautify, 'DD');
};
export const beautifyPastDateRelativeToNow = (
pastDate: Date | string | number,
locale?: Locale,
) => {
try {
const parsedDate = parseDate(pastDate);
const now = new Date();
const diffInSeconds = Math.abs(
(now.getTime() - parsedDate.toJSDate().getTime()) / 1000,
);
// For very recent times (less than 30 seconds), show "now"
if (diffInSeconds < 30) {
return t`now`;
}
return formatDistanceToNow(parsedDate.toJSDate(), {
addSuffix: true,
}).replace('less than a minute ago', 'now');
} catch (error) {
logError(error);
return '';
}
};
export const beautifyPastDateAbsolute = (pastDate: Date | string | number) => {
try {
const parsedPastDate = parseDate(pastDate);
const hoursDiff = parsedPastDate.diffNow('hours').negate().hours;
if (hoursDiff <= 24) {
return parsedPastDate.toFormat('HH:mm');
} else if (hoursDiff <= 7 * 24) {
return parsedPastDate.toFormat('cccc - HH:mm');
} else if (hoursDiff <= 365 * 24) {
return parsedPastDate.toFormat('MMMM d - HH:mm');
} else {
return parsedPastDate.toFormat('dd/MM/yyyy - HH:mm');
}
locale,
includeSeconds: true,
});
} catch (error) {
logError(error);
return '';
@@ -133,96 +134,54 @@ export const beautifyDateDiff = (
date: string,
dateToCompareWith?: string,
short = false,
locale?: Locale,
) => {
// For simple cases, use date-fns which has excellent locale support
if (!short && isDefined(locale)) {
const fromDate = new Date(date);
const toDate = dateToCompareWith ? new Date(dateToCompareWith) : new Date();
return formatDistance(fromDate, toDate, { locale });
}
// Manual implementation for complex cases or when locale is not available
const dateDiff = DateTime.fromISO(date).diff(
dateToCompareWith ? DateTime.fromISO(dateToCompareWith) : DateTime.now(),
['years', 'days'],
);
const years = Math.floor(dateDiff.years);
const days = Math.floor(dateDiff.days);
let result = '';
if (dateDiff.years) result = result + `${dateDiff.years} year`;
if (![0, 1].includes(dateDiff.years)) result = result + 's';
if (short && dateDiff.years) return result;
if (dateDiff.years && dateDiff.days) result = result + ' and ';
if (dateDiff.days) result = result + `${Math.floor(dateDiff.days)} day`;
if (![0, 1].includes(dateDiff.days)) result = result + 's';
if (years !== 0) {
result = plural(Math.abs(years), {
one: `${years} ${t`year`}`,
other: `${years} ${t`years`}`,
});
if (short) return result;
}
if (years !== 0 && days !== 0) {
result += ` ${t`and`} `;
}
if (days !== 0) {
const daysPart = plural(Math.abs(days), {
one: `${days} ${t`day`}`,
other: `${days} ${t`days`}`,
});
result += daysPart;
}
return result;
};
const getMonthLabels = () => {
const formatter = new Intl.DateTimeFormat(undefined, {
month: 'short',
timeZone: 'UTC',
});
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
.map((month) => {
const monthZeroFilled = month < 10 ? `0${month}` : month;
return new Date(`2017-${monthZeroFilled}-01T00:00:00+00:00`);
})
.map((date) => formatter.format(date));
};
export const getMonthLabelsMemoized = moize(getMonthLabels);
export const formatISOStringToHumanReadableDateTime = (date: string) => {
const monthLabels = getMonthLabelsMemoized();
if (!isDefined(monthLabels)) {
return formatToHumanReadableDateTime(date);
}
const year = date.slice(0, 4);
const month = date.slice(5, 7);
const monthLabel = monthLabels[parseInt(month, 10) - 1];
const jsDate = new Date(date);
const day = jsDate.getDate();
const hours = `0${jsDate.getHours()}`.slice(-2);
const minutes = `0${jsDate.getMinutes()}`.slice(-2);
return `${day} ${monthLabel} ${year} - ${hours}:${minutes}`;
};
export const formatISOStringToHumanReadableDate = (date: string) => {
const monthLabels = getMonthLabelsMemoized();
if (!isDefined(monthLabels)) {
return formatToHumanReadableDate(date);
}
const year = date.slice(0, 4);
const month = date.slice(5, 7);
const day = date.slice(8, 10);
const monthLabel = monthLabels[parseInt(month, 10) - 1];
return `${day} ${monthLabel} ${year}`;
};
export const formatToHumanReadableDate = (date: Date | string) => {
const parsedJSDate = parseDate(date).toJSDate();
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(parsedJSDate);
};
export const formatToHumanReadableDateTime = (date: Date | string) => {
const parsedJSDate = parseDate(date).toJSDate();
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
}).format(parsedJSDate);
return i18n.date(parsedJSDate, { dateStyle: 'medium' });
};
export const getDateFormatString = (